├── .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 | ![screenshot](./screenshot.png) 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 | --------------------------------------------------------------------------------