├── .ruby-gemset
├── .ruby-version
├── .rspec
├── screenshot.png
├── lib
├── lazy_api_doc
│ ├── version.rb
│ ├── route_parser.rb
│ ├── variants_parser.rb
│ └── generator.rb
├── generators
│ └── lazy_api_doc
│ │ ├── templates
│ │ ├── public
│ │ │ ├── layout.yml
│ │ │ └── index.html
│ │ └── support
│ │ │ ├── minitest_interceptor.rb
│ │ │ └── rspec_interceptor.rb
│ │ └── install_generator.rb
└── lazy_api_doc.rb
├── .travis.yml
├── Rakefile
├── spec
├── lazy_api_doc_spec.rb
├── .rubocop.yml
├── spec_helper.rb
└── lazy_api_doc
│ ├── generator_spec.rb
│ └── variants_parser_spec.rb
├── bin
├── setup
└── console
├── .gitignore
├── Gemfile
├── docs
└── example
│ ├── layout.yml
│ ├── index.html
│ └── api.yml
├── .rubocop.yml
├── LICENSE.txt
├── lazy_api_doc.gemspec
├── Gemfile.lock
├── README.md
└── CODE_OF_CONDUCT.md
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | lazy_api_doc
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.0.3
2 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bguban/lazy_api_doc/HEAD/screenshot.png
--------------------------------------------------------------------------------
/lib/lazy_api_doc/version.rb:
--------------------------------------------------------------------------------
1 | module LazyApiDoc
2 | VERSION = "0.2.5".freeze
3 | end
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: ruby
3 | cache: bundler
4 | rvm:
5 | - 2.6.4
6 | before_install: gem install bundler -v 2.1.2
7 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rspec/core/rake_task"
3 |
4 | RSpec::Core::RakeTask.new(:spec)
5 |
6 | task default: :spec
7 |
--------------------------------------------------------------------------------
/spec/lazy_api_doc_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe LazyApiDoc do
2 | it "has a version number" do
3 | expect(LazyApiDoc::VERSION).not_to be nil
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 |
10 | # rspec failure tracking
11 | .rspec_status
12 | coverage
13 | *.gem
14 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in lazy_api_doc.gemspec
4 | gemspec
5 |
6 | gem "rake", "~> 12.0"
7 | gem "rspec", "~> 3.0"
8 | gem 'rubocop', require: false
9 | gem "rubocop-rspec", require: false
10 | gem "simplecov", require: false, group: :test
11 |
--------------------------------------------------------------------------------
/docs/example/layout.yml:
--------------------------------------------------------------------------------
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:
15 |
--------------------------------------------------------------------------------
/spec/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_from:
2 | - ../.rubocop.yml
3 |
4 | require:
5 | - rubocop-rspec
6 |
7 | RSpec/ExampleLength:
8 | Enabled: false
9 |
10 | RSpec/DescribedClass:
11 | Enabled: false
12 |
13 | RSpec/FactoryBot/CreateList:
14 | Enabled: false
15 |
16 | RSpec/NamedSubject:
17 | Enabled: false
18 |
--------------------------------------------------------------------------------
/lib/generators/lazy_api_doc/templates/public/layout.yml:
--------------------------------------------------------------------------------
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:
15 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 | require "lazy_api_doc"
5 |
6 | # You can add fixtures and/or initialization code here to make experimenting
7 | # with your gem easier. You can also use a different console, if you like.
8 |
9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 |
13 | require "irb"
14 | IRB.start(__FILE__)
15 |
--------------------------------------------------------------------------------
/lib/generators/lazy_api_doc/templates/support/minitest_interceptor.rb:
--------------------------------------------------------------------------------
1 | module LazyApiDocInterceptor
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | %w[get post patch put head delete].each do |method|
6 | define_method(method) do |*args, **kwargs|
7 | result = super(*args, **kwargs)
8 | # self.class.metadata[:doc]
9 | LazyApiDoc.add_test(self)
10 | result
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/generators/lazy_api_doc/templates/support/rspec_interceptor.rb:
--------------------------------------------------------------------------------
1 | module LazyApiDocInterceptor
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | %w[get post patch put head delete].each do |method|
6 | define_method(method) do |*args, **kwargs|
7 | result = super(*args, **kwargs)
8 | # self.class.metadata[:doc] can be used to document only tests with doc: true metadata
9 | LazyApiDoc.add_spec(self)
10 | result
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 | SimpleCov.start do
3 | track_files 'lib/**/*.rb'
4 | end
5 |
6 | require "bundler/setup"
7 | require "lazy_api_doc"
8 |
9 | RSpec.configure do |config|
10 | # Enable flags like --only-failures and --next-failure
11 | config.example_status_persistence_file_path = ".rspec_status"
12 |
13 | # Disable RSpec exposing methods globally on `Module` and `main`
14 | config.disable_monkey_patching!
15 |
16 | config.expect_with :rspec do |c|
17 | c.syntax = :expect
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/docs/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | My Application Documentation
5 |
6 |
7 |
8 |
9 |
10 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lib/generators/lazy_api_doc/templates/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | My Application Documentation
5 |
6 |
7 |
8 |
9 |
10 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lib/lazy_api_doc/route_parser.rb:
--------------------------------------------------------------------------------
1 | module LazyApiDoc
2 | class RouteParser
3 | def self.find_by(example)
4 | r = routes.find do |r|
5 | r['verb'].include?(example.verb) && example.params.slice(*r['defaults'].keys) == r['defaults']
6 | end
7 | end
8 |
9 | def self.routes
10 | return @routes if defined?(@routes)
11 |
12 | @routes = Rails.application.routes.routes.map { |route| format(route) }
13 | end
14 |
15 | def self.format(route)
16 | route = ActionDispatch::Routing::RouteWrapper.new(route)
17 | {
18 | 'doc_path' => route.path.gsub("(.:format)", "").gsub(/(:\w+)/, '{\1}').delete(":"),
19 | 'path_params' => route.path.gsub("(.:format)", "").scan(/:\w+/).map { |p| p.delete(":") },
20 | 'controller' => route.controller,
21 | 'action' => route.action,
22 | 'verb' => route.verb.split('|'),
23 | 'defaults' => route.defaults.transform_keys(&:to_s)
24 | }
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | # Rubocop rules explanation:
2 | # https://github.com/rubocop-hq/rubocop/blob/master/config/default.yml
3 |
4 | AllCops:
5 | DisplayCopNames: true
6 | TargetRubyVersion: 2.3.0
7 | NewCops: enable
8 | SuggestExtensions: false
9 | Exclude:
10 | - bin/bundle
11 | - spec
12 |
13 | Style/StringLiterals:
14 | Enabled: false
15 |
16 | Style/FrozenStringLiteralComment:
17 | Enabled: false
18 |
19 | Layout/LineLength:
20 | Max: 120
21 |
22 | Style/ExpandPathArguments:
23 | Enabled: false
24 |
25 | Layout/HashAlignment:
26 | Enabled: false
27 |
28 | Metrics/BlockLength:
29 | Exclude:
30 | - spec/**/*
31 |
32 | Metrics/MethodLength:
33 | Max: 50
34 |
35 | Style/Documentation:
36 | Enabled: false
37 |
38 | Metrics/AbcSize:
39 | Max: 30
40 |
41 | Style/WordArray:
42 | Enabled: false
43 |
44 | Style/OpenStructUse:
45 | Enabled: false
46 |
47 | Metrics/PerceivedComplexity:
48 | Max: 10
49 |
50 | Metrics/CyclomaticComplexity:
51 | Max: 10
52 |
53 | Metrics/ClassLength:
54 | Max: 150
55 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Bogdan Guban
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lazy_api_doc.gemspec:
--------------------------------------------------------------------------------
1 | require_relative 'lib/lazy_api_doc/version'
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "lazy_api_doc"
5 | spec.version = LazyApiDoc::VERSION
6 | spec.authors = ["Bogdan Guban"]
7 | spec.email = ["biguban@gmail.com"]
8 |
9 | spec.summary = "Creates openapi v3 documentation based on rspec request tests"
10 | spec.description = <<~EODOC
11 | The gem collects all requests and responses from your request specs and generates documentation based on it
12 | EODOC
13 | spec.homepage = "https://github.com/bguban/lazy_api_doc"
14 | spec.license = "MIT"
15 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
16 |
17 | spec.metadata["allowed_push_host"] = "https://rubygems.org"
18 |
19 | spec.metadata["homepage_uri"] = spec.homepage
20 | spec.metadata["source_code_uri"] = spec.homepage
21 | spec.metadata["changelog_uri"] = spec.homepage
22 |
23 | # Specify which files should be added to the gem when it is released.
24 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
26 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27 | end
28 | spec.bindir = "exe"
29 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30 | spec.require_paths = ["lib"]
31 | spec.metadata['rubygems_mfa_required'] = 'true'
32 | end
33 |
--------------------------------------------------------------------------------
/spec/lazy_api_doc/generator_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe LazyApiDoc::Generator do
2 | describe '#query_params' do
3 | subject do
4 | LazyApiDoc::Generator.new.send(
5 | :query_params,
6 | { 'verb' => ['GET'], 'defaults' => {} },
7 | [OpenStruct.new('request' => { 'full_path' => "http://example.com?#{query}" }, 'params' => { 'f' => [1, 2, 3] },
8 | 'verb' => 'GET')]
9 | )
10 | end
11 |
12 | let(:query) { 'a=&b=1&c[]=1&d[]=2&d[]=3&e[a]=1&e[b]=2' }
13 |
14 | it {
15 | expect(subject).to eq(
16 | [
17 | { "in" => "query", "required" => true, "name" => "a", "schema" => { "type" => "string", "example" => "" } },
18 | { "in" => "query", "required" => true, "name" => "b", "schema" => { "type" => "string", "example" => "1" } },
19 | { "in" => "query", "required" => true, "name" => "c",
20 | "schema" => { "type" => "array", "items" => { "type" => "string", "example" => "1" }, "example" => ["1"] } },
21 | { "in" => "query", "required" => true, "name" => "d",
22 | "schema" => { "type" => "array", "items" => { "type" => "string", "example" => "2" }, "example" => ["2", "3"] } },
23 | # TODO: not sure that object should be treated as several strings
24 | { "in" => "query", "required" => true, "name" => "e[a]",
25 | "schema" => { "type" => "string", "example" => "1" } },
26 | { "in" => "query", "required" => true, "name" => "e[b]",
27 | "schema" => { "type" => "string", "example" => "2" } },
28 | { "in" => "query", "required" => true, "name" => "f",
29 | "schema" => { "type" => "array", "items" => { "type" => "integer", "example" => 1 }, "example" => [1, 2, 3] } }
30 | ]
31 | )
32 | }
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | lazy_api_doc (0.2.5)
5 |
6 | GEM
7 | remote: https://rubygems.org/
8 | specs:
9 | ast (2.4.2)
10 | diff-lcs (1.3)
11 | docile (1.4.0)
12 | json (2.6.3)
13 | parallel (1.22.1)
14 | parser (3.2.1.1)
15 | ast (~> 2.4.1)
16 | rainbow (3.1.1)
17 | rake (12.3.3)
18 | regexp_parser (2.7.0)
19 | rexml (3.2.5)
20 | rspec (3.9.0)
21 | rspec-core (~> 3.9.0)
22 | rspec-expectations (~> 3.9.0)
23 | rspec-mocks (~> 3.9.0)
24 | rspec-core (3.9.0)
25 | rspec-support (~> 3.9.0)
26 | rspec-expectations (3.9.0)
27 | diff-lcs (>= 1.2.0, < 2.0)
28 | rspec-support (~> 3.9.0)
29 | rspec-mocks (3.9.0)
30 | diff-lcs (>= 1.2.0, < 2.0)
31 | rspec-support (~> 3.9.0)
32 | rspec-support (3.9.0)
33 | rubocop (1.48.1)
34 | json (~> 2.3)
35 | parallel (~> 1.10)
36 | parser (>= 3.2.0.0)
37 | rainbow (>= 2.2.2, < 4.0)
38 | regexp_parser (>= 1.8, < 3.0)
39 | rexml (>= 3.2.5, < 4.0)
40 | rubocop-ast (>= 1.26.0, < 2.0)
41 | ruby-progressbar (~> 1.7)
42 | unicode-display_width (>= 2.4.0, < 3.0)
43 | rubocop-ast (1.27.0)
44 | parser (>= 3.2.1.0)
45 | rubocop-rspec (1.38.1)
46 | rubocop (>= 0.68.1)
47 | ruby-progressbar (1.13.0)
48 | simplecov (0.21.2)
49 | docile (~> 1.1)
50 | simplecov-html (~> 0.11)
51 | simplecov_json_formatter (~> 0.1)
52 | simplecov-html (0.12.3)
53 | simplecov_json_formatter (0.1.3)
54 | unicode-display_width (2.4.2)
55 |
56 | PLATFORMS
57 | ruby
58 |
59 | DEPENDENCIES
60 | lazy_api_doc!
61 | rake (~> 12.0)
62 | rspec (~> 3.0)
63 | rubocop
64 | rubocop-rspec
65 | simplecov
66 |
67 | BUNDLED WITH
68 | 2.2.32
69 |
--------------------------------------------------------------------------------
/lib/lazy_api_doc/variants_parser.rb:
--------------------------------------------------------------------------------
1 | module LazyApiDoc
2 | class VariantsParser
3 | OPTIONAL = :lazy_api_doc_optional
4 | attr_reader :variants
5 |
6 | def initialize(variants)
7 | @variants = variants.is_a?(Array) ? variants : [variants]
8 | end
9 |
10 | def result
11 | @result ||= parse(variants.first, variants)
12 | end
13 |
14 | def parse(variant, variants)
15 | variants.delete(OPTIONAL)
16 | case variant
17 | when Array
18 | variant = variants.find(&:any?) || variants.first
19 | parse_array(variant, variants)
20 | when Hash
21 | parse_hash(variants)
22 | else
23 | types_template(variants).merge("example" => variant)
24 | end
25 | end
26 |
27 | def types_template(variants)
28 | types = types_of(variants)
29 | if types.count == 1
30 | {
31 | "type" => types.first
32 | }
33 | else
34 | {
35 | "oneOf" => types.map { |t| { "type" => t } }
36 | }
37 | end
38 | end
39 |
40 | def types_of(variants)
41 | variants.map { |v| type_of(v) }.uniq
42 | end
43 |
44 | def type_of(variant)
45 | case variant
46 | when Hash
47 | "object"
48 | when NilClass
49 | "null"
50 | when TrueClass, FalseClass
51 | "boolean"
52 | when String
53 | type_of_string(variant)
54 | when Float
55 | 'number'
56 | else
57 | variant.class.name.downcase
58 | end
59 | end
60 |
61 | def type_of_string(variant)
62 | case variant
63 | when /\A\d+\.\d+\z/
64 | "decimal"
65 | else
66 | "string"
67 | end
68 | end
69 |
70 | def parse_hash(variants)
71 | result = types_template(variants)
72 | variant = variants.select { |v| v.is_a?(Hash) }.reverse_each
73 | .with_object({}) { |v, res| res.merge!(v) }
74 | result["properties"] = variant.to_h do |key, val|
75 | [
76 | key.to_s,
77 | parse(val, variants.select { |v| v.is_a?(Hash) }.map { |v| v.fetch(key, OPTIONAL) })
78 | ]
79 | end
80 | result["required"] = variant.keys.select { |key| variants.select { |v| v.is_a?(Hash) }.all? { |v| v.key?(key) } }
81 | result
82 | end
83 |
84 | def parse_array(variant, variants)
85 | first = variant.first
86 | types_template(variants).merge(
87 | "items" => parse(first, variants.compact.map(&:first).compact),
88 | "example" => variant
89 | )
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/generators/lazy_api_doc/install_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators'
2 | require 'lazy_api_doc'
3 |
4 | module LazyApiDoc
5 | module Generators
6 | class InstallGenerator < Rails::Generators::Base
7 | source_root File.expand_path('templates', __dir__)
8 |
9 | desc "Copy base configuration for LazyApiDoc"
10 | def install
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
22 |
23 | install_rspec if Dir.exist?('spec')
24 |
25 | install_minitest if Dir.exist?('test')
26 | end
27 |
28 | private
29 |
30 | def install_rspec
31 | copy_file 'support/rspec_interceptor.rb', 'spec/support/lazy_api_doc_interceptor.rb'
32 |
33 | insert_into_file 'spec/rails_helper.rb', after: "RSpec.configure do |config|\n" do
34 | <<-RUBY
35 | if ENV['LAZY_API_DOC']
36 | require 'lazy_api_doc'
37 | require 'support/lazy_api_doc_interceptor'
38 |
39 | config.include LazyApiDocInterceptor, type: :request
40 | config.include LazyApiDocInterceptor, type: :controller
41 |
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
55 | RUBY
56 | end
57 | end
58 |
59 | def install_minitest
60 | copy_file 'support/minitest_interceptor.rb', 'test/support/lazy_api_doc_interceptor.rb'
61 |
62 | append_to_file 'test/test_helper.rb' do
63 | <<~RUBY
64 |
65 | if ENV['LAZY_API_DOC']
66 | require 'lazy_api_doc'
67 | require 'support/lazy_api_doc_interceptor'
68 |
69 | class ActionDispatch::IntegrationTest
70 | include LazyApiDocInterceptor
71 | end
72 |
73 | Minitest.after_run do
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
84 | end
85 | end
86 | RUBY
87 | end
88 | end
89 | end
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LazyApiDoc
2 |
3 | A library to generate OpenAPI V3 documentation from tests. Try out the results
4 | [here](https://bguban.github.io/lazy_api_doc/example).
5 |
6 | LazyApiDoc collects requests and responses from your controller and request specs, retrieves data types, optional
7 | attributes, endpoint description and then generates OpenAPI documentation.
8 |
9 | 
10 |
11 | ## Installation
12 |
13 | Add this line to your application's Gemfile:
14 |
15 | ```ruby
16 | gem 'lazy_api_doc', require: false, group: :test
17 | ```
18 |
19 | And then execute:
20 |
21 | $ bundle install
22 |
23 | Or install it yourself as:
24 |
25 | $ gem install lazy_api_doc
26 |
27 | Then run install task
28 |
29 | $ rails g lazy_api_doc:install
30 |
31 | ## Configuration
32 |
33 | You can customize LazyApiDoc behavior using config file or environment variables.
34 |
35 | ```yaml
36 | # config/lazy_api_doc.yml
37 |
38 | # base directory for storing layout files and generated documentation
39 | path: 'doc/lazy_api_doc' # ENV['LAZY_API_DOC_PATH'] default: 'public/lazy_api_doc'
40 |
41 | # TTL for files generated by running tests in different processes (ParallelTest). Each process generates a file.
42 | # After all the processes are done one of them collects all example files and generates the documentation. In case
43 | # when the example files were not deleted before running the tests, old files will be ignored.
44 | example_file_ttl: 1800 # ENV['LAZY_API_DOC_EXAMPLE_FILE_TTL'] default: 1800 (30 minutes)
45 | ```
46 |
47 | ## Usage
48 |
49 | Update files `public/lazy_api_doc/index.html` and `public/lazy_api_doc/layout.yml`. These files will be
50 | used as templates to show the documentation. You need to set your application name, description and
51 | so on.
52 |
53 | And just run your tests with `DOC=true` environment variable:
54 |
55 | $ LAZY_API_DOC=true rspec
56 |
57 | or
58 |
59 | # LAZY_API_DOC=true rake test
60 |
61 | The documentation will be placed `public/lazy_api_doc/api.yml`. To see it just run server
62 |
63 | $ rails server
64 |
65 | and navigate to http://localhost:3000/lazy_api_doc/
66 |
67 | ## How does it work under the hood?
68 |
69 | LazyApiDoc gathers your test requests and responses, group them by controllers and actions that were affected, and tries to guess the type of every field.
70 | ```json
71 | user: {
72 | id: 1, // Integer
73 | name: "John Doe", // String,
74 | created_at: "2020-05-17 20:15:47Z -0400" // DateTime
75 | }
76 | ```
77 | After that, it just builds an OpenAPI specification based on it.
78 |
79 | ## Contributing
80 |
81 | Bug reports and pull requests are welcome on GitHub at https://github.com/bguban/lazy_api_doc. This project is intended
82 | to be a safe, welcoming space for collaboration, and contributors are expected to adhere to
83 | the [code of conduct](https://github.com/bguban/lazy_api_doc/blob/master/CODE_OF_CONDUCT.md).
84 |
85 |
86 | ## License
87 |
88 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
89 |
90 | ## Code of Conduct
91 |
92 | Everyone interacting in the LazyApiDoc project's codebases, issue trackers, chat rooms and mailing lists is expected to
93 | follow the [code of conduct](https://github.com/bguban/lazy_api_doc/blob/master/CODE_OF_CONDUCT.md).
94 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at biguban@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/spec/lazy_api_doc/variants_parser_spec.rb:
--------------------------------------------------------------------------------
1 | RSpec.describe LazyApiDoc::VariantsParser do
2 | let(:parser) { LazyApiDoc::VariantsParser.new(variants) }
3 |
4 | context "with simple types" do
5 | let(:variants) { [{ a: 1, b: "s", c: "1.1", d: true, e: nil }, { a: 2, b: "v", c: "2.3", d: false, e: nil }] }
6 |
7 | it "returns openapi structure" do
8 | expect(parser.result).to eq(
9 | "type" => "object",
10 | "properties" => {
11 | "a" => { "type" => "integer", "example" => 1 },
12 | "b" => { "type" => "string", "example" => "s" },
13 | "c" => { "type" => "decimal", "example" => "1.1" },
14 | "d" => { "type" => "boolean", "example" => true },
15 | "e" => { "type" => "null", "example" => nil }
16 | },
17 | "required" => %i[a b c d e]
18 | )
19 | end
20 | end
21 |
22 | context "with complex types" do
23 | let(:variants) { [{ a: [1, 2], h: {} }, { a: [3, 4], h: {} }] }
24 |
25 | it "returns openapi structure" do
26 | expect(parser.result).to eq(
27 | "type" => "object",
28 | "properties" => {
29 | "a" => {
30 | "type" => "array",
31 | "items" => { "type" => "integer", "example" => 1 },
32 | "example" => [1, 2]
33 | },
34 | "h" => {
35 | "type" => "object",
36 | "properties" => {},
37 | "required" => []
38 | }
39 | },
40 | "required" => %i[a h]
41 | )
42 | end
43 | end
44 |
45 | context "with mixed types" do
46 | let(:variants) { [{ a: 1 }, { a: "foo" }, {}] }
47 |
48 | it "returns openapi structure" do
49 | expect(parser.result).to eq(
50 | "type" => "object",
51 | "properties" => {
52 | "a" => {
53 | "oneOf" => [{ "type" => "integer" }, { "type" => "string" }],
54 | "example" => 1
55 | }
56 | },
57 | "required" => []
58 | )
59 | end
60 | end
61 |
62 | context "when the first hash doesn't have keys of the second hash" do
63 | let(:variants) { [{ a: 1 }, { b: 'foo' }] }
64 |
65 | it "returns keys for both hashes" do
66 | expect(parser.result).to eq(
67 | "type" => "object",
68 | "properties" => {
69 | "a" => {
70 | "type" => "integer",
71 | "example" => 1
72 | },
73 | "b" => {
74 | "type" => "string",
75 | "example" => "foo"
76 | }
77 | },
78 | "required" => []
79 | )
80 | end
81 | end
82 |
83 | context 'with optional array' do
84 | let(:variants) { [[1], nil] }
85 |
86 | it "returns" do
87 | expect(parser.result).to eq(
88 | "example" => [1],
89 | "items" => { "example" => 1, "type" => "integer" },
90 | "oneOf" => [{ "type" => "array" }, { "type" => "null" }]
91 | )
92 | end
93 | end
94 |
95 | context 'when the first variant has an empty array' do
96 | let(:variants) { [[], [{ foo: 'bar' }]] }
97 |
98 | it "returns" do
99 | expect(parser.result).to eq(
100 | "type" => "array",
101 | "items" => {
102 | "type" => "object",
103 | "properties" => {
104 | "foo" => {
105 | "type" => "string",
106 | "example" => "bar"
107 | }
108 | },
109 | "required" => [:foo]
110 | },
111 | "example" => [{ foo: "bar" }]
112 | )
113 | end
114 | end
115 |
116 | context 'when mix of hash and array' do
117 | let(:variants) { [{ 'a' => 1 }, []] }
118 |
119 | it "returns" do
120 | expect(parser.result).to eq(
121 | {
122 | "oneOf" => [
123 | { "type" => "object" },
124 | { "type" => "array" }
125 | ],
126 | "properties" => {
127 | "a" => {
128 | "example" => 1,
129 | "type" => "integer"
130 | }
131 | },
132 | "required" => ["a"]
133 | }
134 | )
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/lib/lazy_api_doc/generator.rb:
--------------------------------------------------------------------------------
1 | require 'cgi'
2 |
3 | module LazyApiDoc
4 | class Generator
5 | EXCLUDED_PARAMS = ["controller", "action", "format"].freeze
6 |
7 | attr_reader :examples
8 |
9 | def initialize
10 | @examples = []
11 | end
12 |
13 | def add(example)
14 | return if example['controller'] == "anonymous" # don't handle virtual controllers
15 |
16 | @examples << example
17 | end
18 |
19 | def clear
20 | @examples = []
21 | end
22 |
23 | def result
24 | result = {}
25 | @examples.map { |example| OpenStruct.new(example) }.sort_by(&:source_location)
26 | .group_by { |example| ::LazyApiDoc::RouteParser.find_by(example) }
27 | .each do |route, examples|
28 | next if route.nil? # TODO: think about adding such cases to log
29 | first = examples.first
30 |
31 | doc_path = route['doc_path']
32 | result[doc_path] ||= {}
33 | result[doc_path].merge!(example_group(first, examples, route))
34 | end
35 | result
36 | end
37 |
38 | private
39 |
40 | def example_group(example, examples, route)
41 | {
42 | example['verb'].downcase => {
43 | "tags" => [example.controller || 'Ungrouped'],
44 | "description" => example["description"].capitalize,
45 | "summary" => example.action,
46 | "parameters" => path_params(route, examples) + query_params(route, examples),
47 | "requestBody" => body_params(route, examples),
48 | "responses" => examples.group_by { |ex| ex.response['code'] }.transform_values do |variants|
49 | {
50 | "description" => variants.first["description"].capitalize,
51 | "content" => {
52 | example.response['content_type'] => {
53 | "schema" => ::LazyApiDoc::VariantsParser.new(variants.map { |v| parse_body(v.response) }).result
54 | }
55 | }
56 | }
57 | end
58 | }.compact
59 | }
60 | end
61 |
62 | def parse_body(response)
63 | if response['content_type'].match?("json")
64 | JSON.parse(response['body'])
65 | else
66 | "Not a JSON response"
67 | end
68 | rescue JSON::ParserError
69 | response['body']
70 | end
71 |
72 | def path_params(route, examples)
73 | path_variants = examples.map { |example| example.params.slice(*route['path_params']) }
74 | ::LazyApiDoc::VariantsParser.new(path_variants).result["properties"].map do |param_name, schema|
75 | {
76 | 'in' => "path",
77 | 'required' => true,
78 | 'name' => param_name,
79 | 'schema' => schema
80 | }
81 | end
82 | end
83 |
84 | def query_params(route, examples)
85 | query_variants = examples.map do |example|
86 | _path, query = example.request['full_path'].split('?')
87 |
88 | params = if query
89 | CGI.parse(query).to_h { |k, v| [k.gsub('[]', ''), k.match?('\[\]') ? v : v.first] }
90 | else
91 | {}
92 | end
93 | if %w[GET HEAD].include?(example['verb'])
94 | params.merge!(example.params.except(*EXCLUDED_PARAMS, *route['path_params'], *route['defaults'].keys))
95 | end
96 | params
97 | end
98 |
99 | parsed = ::LazyApiDoc::VariantsParser.new(query_variants).result
100 | parsed["properties"].map do |param_name, schema|
101 | {
102 | 'in' => "query",
103 | 'required' => parsed['required'].include?(param_name),
104 | 'name' => param_name,
105 | 'schema' => schema
106 | }
107 | end
108 | end
109 |
110 | def body_params(route, examples)
111 | first = examples.first
112 | return unless %w[POST PATCH PUT DELETE].include?(first['verb'])
113 |
114 | variants = examples.map { |example| example.params.except(*EXCLUDED_PARAMS, *route['path_params'], *route['defaults'].keys) }
115 | {
116 | 'content' => {
117 | first.content_type => {
118 | 'schema' => ::LazyApiDoc::VariantsParser.new(variants).result
119 | }
120 | }
121 | }
122 | end
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/lib/lazy_api_doc.rb:
--------------------------------------------------------------------------------
1 | require "lazy_api_doc/version"
2 | require "lazy_api_doc/variants_parser"
3 | require "lazy_api_doc/generator"
4 | require "lazy_api_doc/route_parser"
5 | require "yaml"
6 |
7 | module LazyApiDoc
8 | class Error < StandardError; end
9 |
10 | class << self
11 | attr_accessor :path, :example_file_ttl
12 |
13 | def configure
14 | yield self
15 | end
16 |
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) : {}
20 |
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
32 |
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_p("#{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
104 | end
105 | end
106 |
107 | LazyApiDoc.reset!
108 |
--------------------------------------------------------------------------------
/docs/example/api.yml:
--------------------------------------------------------------------------------
1 | ---
2 | openapi: 3.0.0
3 | info:
4 | title: App name
5 | version: 1.0.0
6 | contact:
7 | name: User Name
8 | email: user@example.com
9 | url: https://app.example.com
10 | servers:
11 | - url: https://app.example.com
12 | description: description
13 | paths:
14 | "/api/v1/price_alerts":
15 | post:
16 | tags:
17 | - price_alerts
18 | description: Basic create
19 | summary: create
20 | parameters: []
21 | requestBody:
22 | content:
23 | application/vnd.api+json:
24 | schema:
25 | type: object
26 | properties:
27 | data:
28 | type: object
29 | properties:
30 | type:
31 | type: string
32 | example: price_alerts
33 | attributes:
34 | type: object
35 | properties:
36 | operator:
37 | type: string
38 | example: lte
39 | value:
40 | type: integer
41 | example: 29
42 | webhook_url:
43 | type: string
44 | example: https://tqyumd.x.pipedream.net?foo=bar
45 | security_sa_id:
46 | type: integer
47 | example: 14
48 | required:
49 | - operator
50 | - value
51 | - webhook_url
52 | - security_sa_id
53 | required:
54 | - type
55 | - attributes
56 | required:
57 | - data
58 | responses:
59 | 201:
60 | description: Basic create
61 | content:
62 | application/vnd.api+json; charset=utf-8:
63 | schema:
64 | type: object
65 | properties:
66 | data:
67 | type: object
68 | properties:
69 | id:
70 | type: string
71 | example: '3'
72 | type:
73 | type: string
74 | example: price_alerts
75 | attributes:
76 | type: object
77 | properties:
78 | security_sa_id:
79 | type: integer
80 | example: 14
81 | status:
82 | type: string
83 | example: active
84 | operator:
85 | type: string
86 | example: lte
87 | value:
88 | type: number
89 | example: 29.0
90 | webhook_url:
91 | type: string
92 | example: https://en2tqyumd.x.pipedream.net?foo=bar
93 | triggered_at:
94 | type: 'null'
95 | example:
96 | required:
97 | - security_sa_id
98 | - status
99 | - operator
100 | - value
101 | - webhook_url
102 | - triggered_at
103 | relationships:
104 | type: object
105 | properties:
106 | security:
107 | type: object
108 | properties:
109 | links:
110 | type: object
111 | properties:
112 | related:
113 | type: string
114 | example: http://test/api/v1/securities/14
115 | required:
116 | - related
117 | required:
118 | - links
119 | required:
120 | - security
121 | required:
122 | - id
123 | - type
124 | - attributes
125 | - relationships
126 | meta:
127 | type: object
128 | properties: {}
129 | required: []
130 | required:
131 | - data
132 | - meta
133 | get:
134 | tags:
135 | - price_alerts
136 | description: Basic fetch
137 | summary: index
138 | parameters: []
139 | responses:
140 | 200:
141 | description: Basic fetch
142 | content:
143 | application/vnd.api+json; charset=utf-8:
144 | schema:
145 | type: object
146 | properties:
147 | data:
148 | type: array
149 | items:
150 | type: object
151 | properties:
152 | id:
153 | type: string
154 | example: '1'
155 | type:
156 | type: string
157 | example: price_alerts
158 | attributes:
159 | type: object
160 | properties:
161 | security_sa_id:
162 | type: integer
163 | example: 2
164 | status:
165 | type: string
166 | example: active
167 | operator:
168 | type: string
169 | example: lte
170 | value:
171 | type: number
172 | example: 89.0
173 | webhook_url:
174 | type: string
175 | example: https://enf7v32tqyumd.x.pipedream.net?foo=bar
176 | triggered_at:
177 | type: 'null'
178 | example:
179 | required:
180 | - security_sa_id
181 | - status
182 | - operator
183 | - value
184 | - webhook_url
185 | - triggered_at
186 | relationships:
187 | type: object
188 | properties:
189 | security:
190 | type: object
191 | properties:
192 | links:
193 | type: object
194 | properties:
195 | related:
196 | type: string
197 | example: http://test/api/v1/securities/2
198 | required:
199 | - related
200 | required:
201 | - links
202 | required:
203 | - security
204 | required:
205 | - id
206 | - type
207 | - attributes
208 | - relationships
209 | example:
210 | - id: '1'
211 | type: price_alerts
212 | attributes:
213 | security_sa_id: 2
214 | status: active
215 | operator: lte
216 | value: 89.0
217 | webhook_url: https://enf7v32tqyumd.x.pipedream.net?foo=bar
218 | triggered_at:
219 | relationships:
220 | security:
221 | links:
222 | related: http://test/api/v1/securities/2
223 | - id: '2'
224 | type: price_alerts
225 | attributes:
226 | security_sa_id: 3
227 | status: active
228 | operator: lte
229 | value: 60.0
230 | webhook_url: https://enf7v32tqyumd.x.pipedream.net?foo=bar
231 | triggered_at:
232 | relationships:
233 | security:
234 | links:
235 | related: http://test/api/v1/securities/3
236 | meta:
237 | type: object
238 | properties: {}
239 | required: []
240 | required:
241 | - data
242 | - meta
243 | "/api/v1/price_alerts/{id}":
244 | delete:
245 | tags:
246 | - price_alerts
247 | description: Basic destroy
248 | summary: destroy
249 | parameters:
250 | - in: path
251 | required: true
252 | name: id
253 | schema:
254 | type: string
255 | example: '4'
256 | responses:
257 | 200:
258 | description: Basic destroy
259 | content:
260 | application/vnd.api+json; charset=utf-8:
261 | schema:
262 | type: object
263 | properties:
264 | meta:
265 | type: object
266 | properties: {}
267 | required: []
268 | required:
269 | - meta
270 | get:
271 | tags:
272 | - price_alerts
273 | description: Basic fetch
274 | summary: show
275 | parameters:
276 | - in: path
277 | required: true
278 | name: id
279 | schema:
280 | type: string
281 | example: '5'
282 | responses:
283 | 200:
284 | description: Basic fetch
285 | content:
286 | application/vnd.api+json; charset=utf-8:
287 | schema:
288 | type: object
289 | properties:
290 | data:
291 | type: object
292 | properties:
293 | id:
294 | type: string
295 | example: '5'
296 | type:
297 | type: string
298 | example: price_alerts
299 | attributes:
300 | type: object
301 | properties:
302 | security_sa_id:
303 | type: integer
304 | example: 25
305 | status:
306 | type: string
307 | example: active
308 | operator:
309 | type: string
310 | example: gte
311 | value:
312 | type: number
313 | example: 48.0
314 | webhook_url:
315 | type: string
316 | example: https://enf7v32tqyumd.x.pipedream.net?foo=bar
317 | triggered_at:
318 | type: 'null'
319 | example:
320 | required:
321 | - security_sa_id
322 | - status
323 | - operator
324 | - value
325 | - webhook_url
326 | - triggered_at
327 | relationships:
328 | type: object
329 | properties:
330 | security:
331 | type: object
332 | properties:
333 | links:
334 | type: object
335 | properties:
336 | related:
337 | type: string
338 | example: http://test/api/v1/securities/25
339 | required:
340 | - related
341 | required:
342 | - links
343 | required:
344 | - security
345 | required:
346 | - id
347 | - type
348 | - attributes
349 | - relationships
350 | meta:
351 | type: object
352 | properties: {}
353 | required: []
354 | required:
355 | - data
356 | - meta
357 | put:
358 | tags:
359 | - price_alerts
360 | description: Basic update
361 | summary: update
362 | parameters:
363 | - in: path
364 | required: true
365 | name: id
366 | schema:
367 | type: string
368 | example: '18'
369 | responses:
370 | 200:
371 | description: Basic update
372 | content:
373 | application/vnd.api+json; charset=utf-8:
374 | schema:
375 | type: object
376 | properties:
377 | data:
378 | type: object
379 | properties:
380 | id:
381 | type: string
382 | example: '18'
383 | type:
384 | type: string
385 | example: price_alerts
386 | attributes:
387 | type: object
388 | properties:
389 | security_sa_id:
390 | type: integer
391 | example: 68
392 | status:
393 | type: string
394 | example: active
395 | operator:
396 | type: string
397 | example: lte
398 | value:
399 | type: number
400 | example: 40.0
401 | webhook_url:
402 | type: string
403 | example: https://enf7v32tqyumd.x.pipedream.net?foo=bar
404 | triggered_at:
405 | type: 'null'
406 | example:
407 | required:
408 | - security_sa_id
409 | - status
410 | - operator
411 | - value
412 | - webhook_url
413 | - triggered_at
414 | relationships:
415 | type: object
416 | properties:
417 | security:
418 | type: object
419 | properties:
420 | links:
421 | type: object
422 | properties:
423 | related:
424 | type: string
425 | example: http://test/api/v1/securities/68
426 | required:
427 | - related
428 | required:
429 | - links
430 | required:
431 | - security
432 | required:
433 | - id
434 | - type
435 | - attributes
436 | - relationships
437 | meta:
438 | type: object
439 | properties: {}
440 | required: []
441 | required:
442 | - data
443 | - meta
444 | "/api/v1/retrievers":
445 | post:
446 | tags:
447 | - retrievers
448 | description: Basic create
449 | summary: create
450 | parameters: []
451 | requestBody:
452 | content:
453 | application/vnd.api+json:
454 | schema:
455 | type: object
456 | properties:
457 | data:
458 | type: object
459 | properties:
460 | type:
461 | type: string
462 | example: retrievers
463 | attributes:
464 | type: object
465 | properties:
466 | klass_name:
467 | type: string
468 | example: xignite_navs_puller
469 | symbol:
470 | type: string
471 | example: SYMBOL17
472 | security_id:
473 | type: integer
474 | example: 54
475 | required:
476 | - klass_name
477 | - symbol
478 | - security_id
479 | required:
480 | - type
481 | - attributes
482 | required:
483 | - data
484 | responses:
485 | 201:
486 | description: Basic create
487 | content:
488 | application/vnd.api+json; charset=utf-8:
489 | schema:
490 | type: object
491 | properties:
492 | data:
493 | type: object
494 | properties:
495 | id:
496 | type: string
497 | example: '30'
498 | type:
499 | type: string
500 | example: retrievers
501 | attributes:
502 | type: object
503 | properties:
504 | security_id:
505 | type: integer
506 | example: 54
507 | klass_name:
508 | type: string
509 | example: xignite_navs_puller
510 | symbol:
511 | type: string
512 | example: SYMBOL17
513 | required:
514 | - security_id
515 | - klass_name
516 | - symbol
517 | relationships:
518 | type: object
519 | properties:
520 | security:
521 | type: object
522 | properties:
523 | links:
524 | type: object
525 | properties:
526 | related:
527 | type: string
528 | example: http://test/api/v1/securities/54
529 | required:
530 | - related
531 | required:
532 | - links
533 | required:
534 | - security
535 | required:
536 | - id
537 | - type
538 | - attributes
539 | - relationships
540 | meta:
541 | type: object
542 | properties: {}
543 | required: []
544 | required:
545 | - data
546 | - meta
547 | get:
548 | tags:
549 | - retrievers
550 | description: Basic fetch
551 | summary: index
552 | parameters: []
553 | responses:
554 | 200:
555 | description: Basic fetch
556 | content:
557 | application/vnd.api+json; charset=utf-8:
558 | schema:
559 | type: object
560 | properties:
561 | data:
562 | type: array
563 | items:
564 | type: object
565 | properties:
566 | id:
567 | type: string
568 | example: '9'
569 | type:
570 | type: string
571 | example: retrievers
572 | attributes:
573 | type: object
574 | properties:
575 | security_id:
576 | type: integer
577 | example: 16
578 | klass_name:
579 | type: string
580 | example: fin_api_puller
581 | symbol:
582 | type: string
583 | example: SYMBOL6
584 | required:
585 | - security_id
586 | - klass_name
587 | - symbol
588 | relationships:
589 | type: object
590 | properties:
591 | security:
592 | type: object
593 | properties:
594 | links:
595 | type: object
596 | properties:
597 | related:
598 | type: string
599 | example: http://test/api/v1/securities/16
600 | required:
601 | - related
602 | required:
603 | - links
604 | required:
605 | - security
606 | required:
607 | - id
608 | - type
609 | - attributes
610 | - relationships
611 | example:
612 | - id: '9'
613 | type: retrievers
614 | attributes:
615 | security_id: 16
616 | klass_name: fin_api_puller
617 | symbol: SYMBOL6
618 | relationships:
619 | security:
620 | links:
621 | related: http://test/api/v1/securities/16
622 | - id: '10'
623 | type: retrievers
624 | attributes:
625 | security_id: 17
626 | klass_name: xignite_quote_puller
627 | symbol: SYMBOL7
628 | relationships:
629 | security:
630 | links:
631 | related: http://test/api/v1/securities/17
632 | meta:
633 | type: object
634 | properties: {}
635 | required: []
636 | required:
637 | - data
638 | - meta
639 | "/api/v1/retrievers/{id}":
640 | delete:
641 | tags:
642 | - retrievers
643 | description: Basic destroy
644 | summary: destroy
645 | parameters:
646 | - in: path
647 | required: true
648 | name: id
649 | schema:
650 | type: string
651 | example: '31'
652 | responses:
653 | 200:
654 | description: Basic destroy
655 | content:
656 | application/vnd.api+json; charset=utf-8:
657 | schema:
658 | type: object
659 | properties:
660 | meta:
661 | type: object
662 | properties: {}
663 | required: []
664 | required:
665 | - meta
666 | get:
667 | tags:
668 | - retrievers
669 | description: Basic fetch
670 | summary: show
671 | parameters:
672 | - in: path
673 | required: true
674 | name: id
675 | schema:
676 | type: string
677 | example: '5'
678 | responses:
679 | 200:
680 | description: Basic fetch
681 | content:
682 | application/vnd.api+json; charset=utf-8:
683 | schema:
684 | type: object
685 | properties:
686 | data:
687 | type: object
688 | properties:
689 | id:
690 | type: string
691 | example: '5'
692 | type:
693 | type: string
694 | example: retrievers
695 | attributes:
696 | type: object
697 | properties:
698 | security_id:
699 | type: integer
700 | example: 7
701 | klass_name:
702 | type: string
703 | example: fin_api_puller
704 | symbol:
705 | type: string
706 | example: SYMBOL1
707 | required:
708 | - security_id
709 | - klass_name
710 | - symbol
711 | relationships:
712 | type: object
713 | properties:
714 | security:
715 | type: object
716 | properties:
717 | links:
718 | type: object
719 | properties:
720 | related:
721 | type: string
722 | example: http://test/api/v1/securities/7
723 | required:
724 | - related
725 | required:
726 | - links
727 | required:
728 | - security
729 | required:
730 | - id
731 | - type
732 | - attributes
733 | - relationships
734 | meta:
735 | type: object
736 | properties: {}
737 | required: []
738 | required:
739 | - data
740 | - meta
741 | put:
742 | tags:
743 | - retrievers
744 | description: Basic update
745 | summary: update
746 | parameters:
747 | - in: path
748 | required: true
749 | name: id
750 | schema:
751 | type: string
752 | example: '11'
753 | responses:
754 | 200:
755 | description: Basic update
756 | content:
757 | application/vnd.api+json; charset=utf-8:
758 | schema:
759 | type: object
760 | properties:
761 | data:
762 | type: object
763 | properties:
764 | id:
765 | type: string
766 | example: '11'
767 | type:
768 | type: string
769 | example: retrievers
770 | attributes:
771 | type: object
772 | properties:
773 | security_id:
774 | type: integer
775 | example: 21
776 | klass_name:
777 | type: string
778 | example: fin_api_puller
779 | symbol:
780 | type: string
781 | example: SYMBOL9
782 | required:
783 | - security_id
784 | - klass_name
785 | - symbol
786 | relationships:
787 | type: object
788 | properties:
789 | security:
790 | type: object
791 | properties:
792 | links:
793 | type: object
794 | properties:
795 | related:
796 | type: string
797 | example: http://test/api/v1/securities/21
798 | required:
799 | - related
800 | required:
801 | - links
802 | required:
803 | - security
804 | required:
805 | - id
806 | - type
807 | - attributes
808 | - relationships
809 | meta:
810 | type: object
811 | properties: {}
812 | required: []
813 | required:
814 | - data
815 | - meta
816 | "/api/v1/securities":
817 | post:
818 | tags:
819 | - securities
820 | description: Basic create
821 | summary: create
822 | parameters: []
823 | requestBody:
824 | content:
825 | application/vnd.api+json:
826 | schema:
827 | type: object
828 | properties:
829 | data:
830 | type: object
831 | properties:
832 | type:
833 | type: string
834 | example: securities
835 | attributes:
836 | type: object
837 | properties:
838 | type:
839 | type: string
840 | example: stock
841 | sa_id:
842 | type: integer
843 | example: 67
844 | sa_slug:
845 | type: string
846 | example: slug65
847 | primary_retriever_klass_name:
848 | type: string
849 | example: xignite_navs_puller
850 | default_retriever_klass_name:
851 | type: string
852 | example: xignite_quote_puller
853 | required:
854 | - type
855 | - sa_id
856 | - sa_slug
857 | - primary_retriever_klass_name
858 | - default_retriever_klass_name
859 | required:
860 | - type
861 | - attributes
862 | required:
863 | - data
864 | responses:
865 | 201:
866 | description: Basic create
867 | content:
868 | application/vnd.api+json; charset=utf-8:
869 | schema:
870 | type: object
871 | properties:
872 | data:
873 | type: object
874 | properties:
875 | id:
876 | type: string
877 | example: '69'
878 | type:
879 | type: string
880 | example: securities
881 | attributes:
882 | type: object
883 | properties:
884 | type:
885 | type: string
886 | example: stock
887 | sa_id:
888 | type: integer
889 | example: 67
890 | sa_slug:
891 | type: string
892 | example: slug65
893 | primary_retriever_klass_name:
894 | type: string
895 | example: xignite_navs_puller
896 | default_retriever_klass_name:
897 | type: string
898 | example: xignite_quote_puller
899 | required:
900 | - type
901 | - sa_id
902 | - sa_slug
903 | - primary_retriever_klass_name
904 | - default_retriever_klass_name
905 | relationships:
906 | type: object
907 | properties:
908 | retrievers:
909 | type: object
910 | properties:
911 | links:
912 | type: object
913 | properties:
914 | related:
915 | type: string
916 | example: http://test/api/v1/retrievers?filter[security_id]=69
917 | required:
918 | - related
919 | required:
920 | - links
921 | required:
922 | - retrievers
923 | required:
924 | - id
925 | - type
926 | - attributes
927 | - relationships
928 | meta:
929 | type: object
930 | properties: {}
931 | required: []
932 | required:
933 | - data
934 | - meta
935 | get:
936 | tags:
937 | - securities
938 | description: Basic fetch
939 | summary: index
940 | parameters: []
941 | responses:
942 | 200:
943 | description: Basic fetch
944 | content:
945 | application/vnd.api+json; charset=utf-8:
946 | schema:
947 | type: object
948 | properties:
949 | data:
950 | type: array
951 | items:
952 | type: object
953 | properties:
954 | id:
955 | type: string
956 | example: '18'
957 | type:
958 | type: string
959 | example: securities
960 | attributes:
961 | type: object
962 | properties:
963 | type:
964 | type: string
965 | example: mf
966 | sa_id:
967 | type: integer
968 | example: 17
969 | sa_slug:
970 | type: string
971 | example: slug14
972 | primary_retriever_klass_name:
973 | type: string
974 | example: xignite_index_puller
975 | default_retriever_klass_name:
976 | type: string
977 | example: iex_puller
978 | required:
979 | - type
980 | - sa_id
981 | - sa_slug
982 | - primary_retriever_klass_name
983 | - default_retriever_klass_name
984 | relationships:
985 | type: object
986 | properties:
987 | retrievers:
988 | type: object
989 | properties:
990 | links:
991 | type: object
992 | properties:
993 | related:
994 | type: string
995 | example: http://test/api/v1/retrievers?filter[security_id]=18
996 | required:
997 | - related
998 | required:
999 | - links
1000 | required:
1001 | - retrievers
1002 | required:
1003 | - id
1004 | - type
1005 | - attributes
1006 | - relationships
1007 | example:
1008 | - id: '18'
1009 | type: securities
1010 | attributes:
1011 | type: mf
1012 | sa_id: 17
1013 | sa_slug: slug14
1014 | primary_retriever_klass_name: xignite_index_puller
1015 | default_retriever_klass_name: iex_puller
1016 | relationships:
1017 | retrievers:
1018 | links:
1019 | related: http://test/api/v1/retrievers?filter[security_id]=18
1020 | - id: '19'
1021 | type: securities
1022 | attributes:
1023 | type: crypto
1024 | sa_id: 18
1025 | sa_slug: slug15
1026 | primary_retriever_klass_name: xignite_quote_puller
1027 | default_retriever_klass_name: iex_puller
1028 | relationships:
1029 | retrievers:
1030 | links:
1031 | related: http://test/api/v1/retrievers?filter[security_id]=19
1032 | meta:
1033 | type: object
1034 | properties: {}
1035 | required: []
1036 | required:
1037 | - data
1038 | - meta
1039 | "/api/v1/securities/{id}":
1040 | delete:
1041 | tags:
1042 | - securities
1043 | description: Basic destroy
1044 | summary: destroy
1045 | parameters:
1046 | - in: path
1047 | required: true
1048 | name: id
1049 | schema:
1050 | type: string
1051 | example: '55'
1052 | responses:
1053 | 200:
1054 | description: Basic destroy
1055 | content:
1056 | application/vnd.api+json; charset=utf-8:
1057 | schema:
1058 | type: object
1059 | properties:
1060 | meta:
1061 | type: object
1062 | properties: {}
1063 | required: []
1064 | required:
1065 | - meta
1066 | get:
1067 | tags:
1068 | - securities
1069 | description: Basic fetch
1070 | summary: show
1071 | parameters:
1072 | - in: path
1073 | required: true
1074 | name: id
1075 | schema:
1076 | type: string
1077 | example: '29'
1078 | responses:
1079 | 200:
1080 | description: Basic fetch
1081 | content:
1082 | application/vnd.api+json; charset=utf-8:
1083 | schema:
1084 | type: object
1085 | properties:
1086 | data:
1087 | type: object
1088 | properties:
1089 | id:
1090 | type: string
1091 | example: '29'
1092 | type:
1093 | type: string
1094 | example: securities
1095 | attributes:
1096 | type: object
1097 | properties:
1098 | type:
1099 | type: string
1100 | example: stock
1101 | sa_id:
1102 | type: integer
1103 | example: 28
1104 | sa_slug:
1105 | type: string
1106 | example: slug25
1107 | primary_retriever_klass_name:
1108 | type: string
1109 | example: trading_economics_puller
1110 | default_retriever_klass_name:
1111 | type: string
1112 | example: trading_economics_puller
1113 | required:
1114 | - type
1115 | - sa_id
1116 | - sa_slug
1117 | - primary_retriever_klass_name
1118 | - default_retriever_klass_name
1119 | relationships:
1120 | type: object
1121 | properties:
1122 | retrievers:
1123 | type: object
1124 | properties:
1125 | links:
1126 | type: object
1127 | properties:
1128 | related:
1129 | type: string
1130 | example: http://test/api/v1/retrievers?filter[security_id]=29
1131 | required:
1132 | - related
1133 | required:
1134 | - links
1135 | required:
1136 | - retrievers
1137 | required:
1138 | - id
1139 | - type
1140 | - attributes
1141 | - relationships
1142 | meta:
1143 | type: object
1144 | properties: {}
1145 | required: []
1146 | required:
1147 | - data
1148 | - meta
1149 | put:
1150 | tags:
1151 | - securities
1152 | description: Basic update
1153 | summary: update
1154 | parameters:
1155 | - in: path
1156 | required: true
1157 | name: id
1158 | schema:
1159 | type: string
1160 | example: '20'
1161 | responses:
1162 | 200:
1163 | description: Basic update
1164 | content:
1165 | application/vnd.api+json; charset=utf-8:
1166 | schema:
1167 | type: object
1168 | properties:
1169 | data:
1170 | type: object
1171 | properties:
1172 | id:
1173 | type: string
1174 | example: '20'
1175 | type:
1176 | type: string
1177 | example: securities
1178 | attributes:
1179 | type: object
1180 | properties:
1181 | type:
1182 | type: string
1183 | example: mf
1184 | sa_id:
1185 | type: integer
1186 | example: 19
1187 | sa_slug:
1188 | type: string
1189 | example: slug16
1190 | primary_retriever_klass_name:
1191 | type: string
1192 | example: xignite_navs_puller
1193 | default_retriever_klass_name:
1194 | type: string
1195 | example: trading_economics_puller
1196 | required:
1197 | - type
1198 | - sa_id
1199 | - sa_slug
1200 | - primary_retriever_klass_name
1201 | - default_retriever_klass_name
1202 | relationships:
1203 | type: object
1204 | properties:
1205 | retrievers:
1206 | type: object
1207 | properties:
1208 | links:
1209 | type: object
1210 | properties:
1211 | related:
1212 | type: string
1213 | example: http://test/api/v1/retrievers?filter[security_id]=20
1214 | required:
1215 | - related
1216 | required:
1217 | - links
1218 | required:
1219 | - retrievers
1220 | required:
1221 | - id
1222 | - type
1223 | - attributes
1224 | - relationships
1225 | meta:
1226 | type: object
1227 | properties: {}
1228 | required: []
1229 | required:
1230 | - data
1231 | - meta
1232 | "/real_time_quotes":
1233 | get:
1234 | tags:
1235 | - real_time_quotes
1236 | description: Get /index
1237 | summary: index
1238 | parameters:
1239 | - in: query
1240 | required: true
1241 | name: sa_ids
1242 | schema:
1243 | type: string
1244 | example: '7'
1245 | responses:
1246 | 200:
1247 | description: Get /index
1248 | content:
1249 | application/json; charset=utf-8:
1250 | schema:
1251 | type: object
1252 | properties:
1253 | real_time_quotes:
1254 | type: array
1255 | items:
1256 | type: object
1257 | properties:
1258 | sa_id:
1259 | type: integer
1260 | example: 7
1261 | high:
1262 | type: number
1263 | example: 76.77
1264 | low:
1265 | type: number
1266 | example: 60.31
1267 | open:
1268 | type: number
1269 | example: 73.7
1270 | close:
1271 | type: number
1272 | example: 74.0
1273 | prev_close:
1274 | type: number
1275 | example: 68.91
1276 | last:
1277 | type: number
1278 | example: 61.39
1279 | volume:
1280 | type: integer
1281 | example: 151
1282 | last_time:
1283 | type: string
1284 | example: '2022-05-06T16:30:04.311-04:00'
1285 | market_cap:
1286 | type: integer
1287 | example: 842898728
1288 | ext_time:
1289 | type: string
1290 | example: '2022-05-06T16:30:04.311-04:00'
1291 | ext_price:
1292 | type: number
1293 | example: 67.95
1294 | ext_market:
1295 | type: string
1296 | example: pre
1297 | updated_at:
1298 | type: string
1299 | example: '2022-05-06T16:30:04.311-04:00'
1300 | required:
1301 | - sa_id
1302 | - high
1303 | - low
1304 | - open
1305 | - close
1306 | - prev_close
1307 | - last
1308 | - volume
1309 | - last_time
1310 | - market_cap
1311 | - ext_time
1312 | - ext_price
1313 | - ext_market
1314 | - updated_at
1315 | example:
1316 | - sa_id: 7
1317 | high: 76.77
1318 | low: 60.31
1319 | open: 73.7
1320 | close: 74.0
1321 | prev_close: 68.91
1322 | last: 61.39
1323 | volume: 151
1324 | last_time: '2022-05-06T16:30:04.311-04:00'
1325 | market_cap: 842898728
1326 | ext_time: '2022-05-06T16:30:04.311-04:00'
1327 | ext_price: 67.95
1328 | ext_market: pre
1329 | updated_at: '2022-05-06T16:30:04.311-04:00'
1330 | required:
1331 | - real_time_quotes
1332 | "/status":
1333 | get:
1334 | tags:
1335 | - system_status
1336 | description: Systemstatuscontroller
1337 | summary: status
1338 | parameters: []
1339 | responses:
1340 | 200:
1341 | description: Systemstatuscontroller
1342 | content:
1343 | application/json; charset=utf-8:
1344 | schema:
1345 | type: object
1346 | properties:
1347 | status:
1348 | type: string
1349 | example: ok
1350 | time:
1351 | type: string
1352 | example: '2022-05-06T16:30:04.901-04:00'
1353 | required:
1354 | - status
1355 | - time
1356 |
--------------------------------------------------------------------------------