├── spec
├── apps
│ ├── rails
│ │ ├── log
│ │ │ └── .keep
│ │ ├── tmp
│ │ │ ├── .keep
│ │ │ └── pids
│ │ │ │ └── .keep
│ │ ├── vendor
│ │ │ ├── .keep
│ │ │ └── my_engine
│ │ │ │ ├── lib
│ │ │ │ └── my_engine
│ │ │ │ │ └── engine.rb
│ │ │ │ └── config
│ │ │ │ └── routes.rb
│ │ ├── lib
│ │ │ ├── assets
│ │ │ │ └── .keep
│ │ │ ├── tasks
│ │ │ │ └── .keep
│ │ │ └── rack_test
│ │ │ │ └── app.rb
│ │ ├── storage
│ │ │ └── .keep
│ │ ├── public
│ │ │ ├── favicon.ico
│ │ │ ├── apple-touch-icon.png
│ │ │ ├── apple-touch-icon-precomposed.png
│ │ │ ├── robots.txt
│ │ │ ├── 500.html
│ │ │ ├── 422.html
│ │ │ └── 404.html
│ │ ├── app
│ │ │ ├── assets
│ │ │ │ ├── images
│ │ │ │ │ └── .keep
│ │ │ │ ├── config
│ │ │ │ │ └── manifest.js
│ │ │ │ └── stylesheets
│ │ │ │ │ └── application.css
│ │ │ ├── models
│ │ │ │ ├── concerns
│ │ │ │ │ └── .keep
│ │ │ │ └── application_record.rb
│ │ │ ├── controllers
│ │ │ │ ├── concerns
│ │ │ │ │ └── .keep
│ │ │ │ ├── secret_items_controller.rb
│ │ │ │ ├── application_controller.rb
│ │ │ │ ├── masters
│ │ │ │ │ └── extensions_controller.rb
│ │ │ │ ├── pages_controller.rb
│ │ │ │ ├── invalid_responses_controller.rb
│ │ │ │ ├── additional_properties_controller.rb
│ │ │ │ ├── sites_controller.rb
│ │ │ │ ├── images_controller.rb
│ │ │ │ ├── users_controller.rb
│ │ │ │ ├── tables_controller.rb
│ │ │ │ └── array_hashes_controller.rb
│ │ │ ├── views
│ │ │ │ └── layouts
│ │ │ │ │ ├── mailer.text.erb
│ │ │ │ │ ├── mailer.html.erb
│ │ │ │ │ └── application.html.erb
│ │ │ ├── helpers
│ │ │ │ └── application_helper.rb
│ │ │ ├── channels
│ │ │ │ └── application_cable
│ │ │ │ │ ├── channel.rb
│ │ │ │ │ └── connection.rb
│ │ │ ├── mailers
│ │ │ │ └── application_mailer.rb
│ │ │ ├── javascript
│ │ │ │ ├── channels
│ │ │ │ │ ├── index.js
│ │ │ │ │ └── consumer.js
│ │ │ │ └── packs
│ │ │ │ │ └── application.js
│ │ │ └── jobs
│ │ │ │ └── application_job.rb
│ │ ├── config
│ │ │ ├── initializers
│ │ │ │ ├── rack_test.rb
│ │ │ │ ├── mime_types.rb
│ │ │ │ ├── filter_parameter_logging.rb
│ │ │ │ ├── application_controller_renderer.rb
│ │ │ │ ├── cookies_serializer.rb
│ │ │ │ ├── backtrace_silencers.rb
│ │ │ │ ├── wrap_parameters.rb
│ │ │ │ ├── inflections.rb
│ │ │ │ └── content_security_policy.rb
│ │ │ ├── spring.rb
│ │ │ ├── boot.rb
│ │ │ ├── environment.rb
│ │ │ ├── cable.yml
│ │ │ ├── credentials.yml.enc
│ │ │ ├── database.yml
│ │ │ ├── locales
│ │ │ │ └── en.yml
│ │ │ ├── storage.yml
│ │ │ ├── application.rb
│ │ │ ├── puma.rb
│ │ │ ├── routes.rb
│ │ │ └── environments
│ │ │ │ ├── test.rb
│ │ │ │ ├── development.rb
│ │ │ │ └── production.rb
│ │ ├── doc
│ │ │ ├── screenshot.png
│ │ │ └── smart
│ │ │ │ └── openapi.yaml
│ │ ├── bin
│ │ │ ├── rake
│ │ │ ├── rails
│ │ │ ├── yarn
│ │ │ └── setup
│ │ ├── config.ru
│ │ ├── package.json
│ │ ├── Rakefile
│ │ ├── db
│ │ │ └── seeds.rb
│ │ └── .gitignore
│ ├── hanami
│ │ ├── lib
│ │ │ ├── tasks
│ │ │ │ └── .keep
│ │ │ ├── hanami_test
│ │ │ │ └── types.rb
│ │ │ └── rack_test
│ │ │ │ └── app.rb
│ │ ├── app
│ │ │ ├── actions
│ │ │ │ ├── .keep
│ │ │ │ ├── extensions
│ │ │ │ │ ├── index.rb
│ │ │ │ │ └── create.rb
│ │ │ │ ├── sites
│ │ │ │ │ ├── show.rb
│ │ │ │ │ ├── site_repository.rb
│ │ │ │ │ └── site_action.rb
│ │ │ │ ├── users
│ │ │ │ │ ├── show.rb
│ │ │ │ │ ├── active.rb
│ │ │ │ │ ├── user_action.rb
│ │ │ │ │ ├── create.rb
│ │ │ │ │ └── user_repository.rb
│ │ │ │ ├── tables
│ │ │ │ │ ├── show.rb
│ │ │ │ │ ├── update.rb
│ │ │ │ │ ├── destroy.rb
│ │ │ │ │ ├── index.rb
│ │ │ │ │ ├── create.rb
│ │ │ │ │ ├── table_action.rb
│ │ │ │ │ └── table_repository.rb
│ │ │ │ ├── secret_items
│ │ │ │ │ └── index.rb
│ │ │ │ ├── array_hashes
│ │ │ │ │ ├── empty_array.rb
│ │ │ │ │ ├── non_hash_items.rb
│ │ │ │ │ ├── single_item.rb
│ │ │ │ │ ├── non_nullable.rb
│ │ │ │ │ ├── nested_arrays.rb
│ │ │ │ │ ├── nullable.rb
│ │ │ │ │ ├── nested.rb
│ │ │ │ │ ├── mixed_types_nested.rb
│ │ │ │ │ └── nested_objects.rb
│ │ │ │ └── images
│ │ │ │ │ ├── index.rb
│ │ │ │ │ ├── show.rb
│ │ │ │ │ ├── upload_nested.rb
│ │ │ │ │ ├── upload_multiple_nested.rb
│ │ │ │ │ ├── upload.rb
│ │ │ │ │ └── upload_multiple.rb
│ │ │ └── action.rb
│ │ ├── .gitignore
│ │ ├── slices
│ │ │ └── my_engine
│ │ │ │ ├── actions
│ │ │ │ ├── .keep
│ │ │ │ └── eng
│ │ │ │ │ └── example.rb
│ │ │ │ └── action.rb
│ │ ├── Procfile.dev
│ │ ├── Rakefile
│ │ ├── test.png
│ │ ├── config.ru
│ │ ├── bin
│ │ │ └── dev
│ │ ├── config
│ │ │ ├── app.rb
│ │ │ ├── settings.rb
│ │ │ ├── puma.rb
│ │ │ └── routes.rb
│ │ └── public
│ │ │ ├── 500.html
│ │ │ └── 404.html
│ └── roda
│ │ ├── doc
│ │ ├── rspec_openapi.rb
│ │ ├── minitest_openapi.yaml
│ │ ├── rspec_openapi.yaml
│ │ ├── minitest_openapi.json
│ │ └── rspec_openapi.json
│ │ └── roda_app.rb
├── integration_tests
│ └── roda_test.rb
├── requests
│ ├── roda_spec.rb
│ ├── rspec_hook_error_spec.rb
│ └── rails_smart_merge_spec.rb
├── rspec
│ ├── rack_test_spec.rb
│ ├── hanami_spec.rb
│ ├── schema_file_spec.rb
│ ├── scheme_merger_spec.rb
│ └── rails_spec.rb
├── minitest
│ ├── rack_test_spec.rb
│ └── rails_spec.rb
└── spec_helper.rb
├── test.png
├── .rspec
├── lib
└── rspec
│ ├── openapi
│ ├── extractors.rb
│ ├── version.rb
│ ├── shared_hooks.rb
│ ├── default_schema.rb
│ ├── key_transformer.rb
│ ├── rspec_hooks.rb
│ ├── schema_sorter.rb
│ ├── schema_file.rb
│ ├── hash_helper.rb
│ ├── minitest_hooks.rb
│ ├── record.rb
│ ├── extractors
│ │ ├── rack.rb
│ │ ├── rails.rb
│ │ └── hanami.rb
│ ├── result_recorder.rb
│ ├── record_builder.rb
│ ├── schema_merger.rb
│ ├── components_updater.rb
│ └── schema_cleaner.rb
│ └── openapi.rb
├── bin
├── setup
└── console
├── .github
├── dependabot.yml
├── release.yaml
└── workflows
│ ├── rubocop.yml
│ ├── codeql-analysis.yml
│ ├── publish.yml
│ ├── test.yml
│ └── create_release.yml
├── .gitignore
├── Rakefile
├── scripts
├── rspec
└── rspec_with_simplecov
├── .simplecov_spawn.rb
├── .rubocop.yml
├── Gemfile
├── LICENSE.txt
├── rspec-openapi.gemspec
├── .rubocop_todo.yml
└── CHANGELOG.md
/spec/apps/rails/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/hanami/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/tmp/pids/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/hanami/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | log/*
3 |
--------------------------------------------------------------------------------
/spec/apps/hanami/slices/my_engine/actions/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/rails/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/apps/hanami/Procfile.dev:
--------------------------------------------------------------------------------
1 | web: bundle exec hanami server
2 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/exoego/rspec-openapi/HEAD/test.png
--------------------------------------------------------------------------------
/spec/apps/rails/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/spec/apps/hanami/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "hanami/rake_tasks"
4 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/initializers/rack_test.rb:
--------------------------------------------------------------------------------
1 | require Rails.root.join('lib/rack_test/app')
2 |
--------------------------------------------------------------------------------
/spec/apps/hanami/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/exoego/rspec-openapi/HEAD/spec/apps/hanami/test.png
--------------------------------------------------------------------------------
/spec/apps/hanami/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'hanami/boot'
4 |
5 | run Hanami.app
6 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --exclude-pattern spec/requests/**/*_spec.rb
2 | --format documentation
3 | --require rspec/openapi
4 | --color
5 |
--------------------------------------------------------------------------------
/spec/apps/rails/doc/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/exoego/rspec-openapi/HEAD/spec/apps/rails/doc/screenshot.png
--------------------------------------------------------------------------------
/spec/apps/rails/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative '../config/boot'
3 | require 'rake'
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/spec/apps/rails/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/extractors.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Create namespace
4 | module RSpec::OpenAPI::Extractors
5 | end
6 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RSpec
4 | module OpenAPI
5 | VERSION = '0.21.4'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/spring.rb:
--------------------------------------------------------------------------------
1 | Spring.watch(
2 | ".ruby-version",
3 | ".rbenv-vars",
4 | "tmp/restart.txt",
5 | "tmp/caching-dev.txt",
6 | )
7 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/spec/apps/roda/doc/rspec_openapi.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::OpenAPI.title = 'Override title'
4 | RSpec::OpenAPI.application_version = '7.7.7'
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/spec/apps/rails/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/spec/apps/rails/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path('../config/application', __dir__)
3 | require_relative '../config/boot'
4 | require 'rails/commands'
5 |
--------------------------------------------------------------------------------
/spec/apps/hanami/slices/my_engine/action.rb:
--------------------------------------------------------------------------------
1 | # auto_register: false
2 | # frozen_string_literal: true
3 |
4 | module MyEngine
5 | class Action < HanamiTest::Action
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/secret_items_controller.rb:
--------------------------------------------------------------------------------
1 | class SecretItemsController < ApplicationController
2 | def index
3 | render json: { items: ['secrets'] }
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 | labels:
8 | - "chore"
9 |
--------------------------------------------------------------------------------
/spec/apps/rails/vendor/my_engine/lib/my_engine/engine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module MyEngine
4 | class Engine < ::Rails::Engine
5 | isolate_namespace MyEngine
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/apps/hanami/bin/dev:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | if ! gem list foreman -i --silent; then
4 | echo "Installing foreman..."
5 | gem install foreman
6 | fi
7 |
8 | exec foreman start -f Procfile.dev "$@"
9 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/spec/apps/hanami/config/app.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'hanami'
4 |
5 | module HanamiTest
6 | class App < Hanami::App
7 | config.middleware.use :body_parser, :json
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/action.rb:
--------------------------------------------------------------------------------
1 | # auto_register: false
2 | # frozen_string_literal: true
3 |
4 | require 'hanami/action'
5 |
6 | class HanamiTest::Action < Hanami::Action
7 | class RecordNotFound < StandardError; end
8 | end
9 |
--------------------------------------------------------------------------------
/spec/apps/rails/vendor/my_engine/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | MyEngine::Engine.routes.draw do
4 | get '/eng_route' => ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['AN ENGINE TEST']] }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/apps/hanami/lib/hanami_test/types.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "dry/types"
4 |
5 | module HanamiTest
6 | Types = Dry.Types
7 |
8 | module Types
9 | # Define your custom types here
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | /Gemfile.lock
10 | /vendor/
11 | .DS_Store
12 |
13 | # rspec failure tracking
14 | .rspec_status
15 |
16 | spec/apps/rails/log/
17 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'bundler/gem_tasks'
4 | require 'rspec/core/rake_task'
5 |
6 | RSpec::Core::RakeTask.new(:spec) do |t|
7 | t.pattern = 'spec/rspec/openapi/**/*_spec.rb'
8 | end
9 |
10 | task default: :spec
11 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: railsapp_production
11 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/spec/apps/rails/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "railsapp",
3 | "private": true,
4 | "dependencies": {
5 | "@rails/ujs": "^6.0.0",
6 | "@rails/activestorage": "^6.0.0",
7 | "@rails/actioncable": "^6.0.0"
8 | },
9 | "version": "0.1.0"
10 | }
11 |
--------------------------------------------------------------------------------
/spec/apps/rails/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | NotFoundError = Class.new(StandardError)
3 |
4 | rescue_from NotFoundError do
5 | render json: { message: 'not found' }, status: 404
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/javascript/channels/index.js:
--------------------------------------------------------------------------------
1 | // Load all the channels within this directory and all subdirectories.
2 | // Channel files must be named *_channel.js.
3 |
4 | const channels = require.context('.', true, /_channel\.js$/)
5 | channels.keys().forEach(channels)
6 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/masters/extensions_controller.rb:
--------------------------------------------------------------------------------
1 | class Masters::ExtensionsController < ApplicationController
2 | def index
3 | render json: [{ name: 'my-ext-1' }]
4 | end
5 |
6 | def create
7 | render json: { message: 'created' }
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/apps/hanami/config/settings.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | class Settings < Hanami::Settings
5 | # Define your app settings here, for example:
6 | #
7 | # setting :my_flag, default: false, constructor: Types::Params::Bool
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/javascript/channels/consumer.js:
--------------------------------------------------------------------------------
1 | // Action Cable provides the framework to deal with WebSockets in Rails.
2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command.
3 |
4 | import { createConsumer } from "@rails/actioncable"
5 |
6 | export default createConsumer()
7 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | # Automatically retry jobs that encountered a deadlock
3 | # retry_on ActiveRecord::Deadlocked
4 |
5 | # Most jobs are safe to ignore if the underlying records are no longer available
6 | # discard_on ActiveJob::DeserializationError
7 | end
8 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/pages_controller.rb:
--------------------------------------------------------------------------------
1 | class PagesController < ApplicationController
2 | def get
3 | if params[:head] == '1'
4 | head :no_content
5 | else
6 | render html: 'HelloHello'.html_safe
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/apps/rails/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_ROOT = File.expand_path('..', __dir__)
3 | Dir.chdir(APP_ROOT) do
4 | exec "yarnpkg", *ARGV
5 | rescue Errno::ENOENT
6 | $stderr.puts "Yarn executable was not detected in the system."
7 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
8 | exit 1
9 | end
10 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/invalid_responses_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class InvalidResponsesController < ApplicationController
4 | def show
5 | render json: {
6 | payload: {
7 | message: 'invalid payload example',
8 | requested_at: Time.current,
9 | },
10 | }
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/extensions/index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Extensions
6 | class Index < HanamiTest::Action
7 | def handle(_request, response)
8 | response.body = { message: 'created' }.to_json
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/additional_properties_controller.rb:
--------------------------------------------------------------------------------
1 | class AdditionalPropertiesController < ApplicationController
2 | def index
3 | response = {
4 | required_key: 'value',
5 | variadic_key: {
6 | gold: 1,
7 | silver: 2,
8 | bronze: 3
9 | }
10 | }
11 | render json: response
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/extensions/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Extensions
6 | class Create < HanamiTest::Action
7 | def handle(_request, response)
8 | response.body = [{ name: 'my-ext-1' }].to_json
9 | end
10 | end
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/sites/show.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Sites
6 | class Show < SiteAction
7 | format :json
8 |
9 | def handle(request, response)
10 | response.body = find_site(request.params[:name]).to_json
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/users/show.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Users
6 | class Show < UserAction
7 | format :json
8 |
9 | def handle(request, response)
10 | response.body = find_user(request.params[:id]).to_json
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/tables/show.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Tables
6 | class Show < TableAction
7 | format :json
8 |
9 | def handle(request, response)
10 | response.body = find_table(request.params[:id]).to_json
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Railsapp
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_link_tag 'application', media: 'all' %>
9 | <%= javascript_pack_tag 'application' %>
10 |
11 |
12 |
13 | <%= yield %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/scripts/rspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | # (The MIT License)
5 | # Copyright (c) 2012 Chad Humphries, David Chelimsky, Myron Marston
6 | # Copyright (c) 2009 Chad Humphries, David Chelimsky
7 | # Copyright (c) 2006 David Chelimsky, The RSpec Development Team
8 | # Copyright (c) 2005 Steven Baker
9 |
10 | require 'rspec/core'
11 | RSpec::Core::Runner.invoke
12 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/secret_items/index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module SecretItems
6 | class Index < HanamiTest::Action
7 | format :json
8 |
9 | def handle(_request, response)
10 | response.body = { items: ['secrets'] }.to_json
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/tables/update.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Tables
6 | class Update < TableAction
7 | def handle(request, response)
8 | response.format = :json
9 | response.body = find_table(request.params[:id]).to_json
10 | end
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/sites_controller.rb:
--------------------------------------------------------------------------------
1 | class SitesController < ApplicationController
2 | def show
3 | render json: find_site(params[:name])
4 | end
5 |
6 | private
7 |
8 | def find_site(name = nil)
9 | case name
10 | when 'abc123', nil
11 | {
12 | name: name,
13 | }
14 | else
15 | raise NotFoundError
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/apps/hanami/slices/my_engine/actions/eng/example.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module MyEngine
4 | module Actions
5 | module Eng
6 | class Example < MyEngine::Action
7 | def handle(_request, response)
8 | response.headers['Content-Type'] = 'text/plain'
9 | response.body = 'AN ENGINE TEST'
10 | end
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/apps/rails/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
7 | # Character.create(name: 'Luke', movie: movies.first)
8 |
--------------------------------------------------------------------------------
/spec/apps/roda/roda_app.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'roda'
4 |
5 | class RodaApp < Roda
6 | plugin :json, classes: [Array, Hash]
7 |
8 | route do |r|
9 | r.on 'roda' do
10 | # POST /roda
11 | r.post do
12 | params = JSON.parse(request.body.read, symbolize_names: true)
13 | params.merge({ name: 'hello' })
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/users/active.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Users
6 | class Active < UserAction
7 | format :json
8 |
9 | def handle(request, response)
10 | response.body = find_user(request.params[:id]).present?.to_json # present not exist in hanami
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/array_hashes/empty_array.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module ArrayHashes
6 | class EmptyArray < HanamiTest::Action
7 | def handle(request, response)
8 | response.format = :json
9 |
10 | response.body = {
11 | "items" => []
12 | }.to_json
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'bundler/setup'
5 | require 'rspec/openapi'
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require 'irb'
15 | IRB.start(__FILE__)
16 |
--------------------------------------------------------------------------------
/spec/apps/rails/lib/rack_test/app.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RackTest
4 | class App
5 | def call(env)
6 | req = Rack::Request.new(env)
7 | path = req.path_info
8 |
9 | case path
10 | when "/foo"
11 | [200, { 'Content-Type' => 'text/plain' }, ['A RACK FOO']]
12 | when "/bar"
13 | [200, { 'Content-Type' => 'text/plain' }, ['A RACK BAR']]
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/array_hashes/non_hash_items.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module ArrayHashes
6 | class NonHashItems < HanamiTest::Action
7 | def handle(request, response)
8 | response.format = :json
9 |
10 | response.body = {
11 | "items" => ["string1", "string2", "string3"]
12 | }.to_json
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/.simplecov_spawn.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | unless ENV['COVERAGE'] && ENV['COVERAGE'].empty?
4 | require 'simplecov'
5 | require 'simplecov-cobertura'
6 |
7 | SimpleCov.at_fork.call(Process.pid)
8 | SimpleCov.formatter SimpleCov::Formatter::MultiFormatter.new([
9 | SimpleCov::Formatter::CoberturaFormatter,
10 | ])
11 | SimpleCov.start do
12 | enable_coverage :branch
13 | add_filter '/spec/'
14 | add_filter '/scripts/'
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | S2PiCpOvCm4PIsh+QqHIxoI2ALTsQI4DoJLbB8i01jlV6/U2ZfFvNU12hWc027xI9180jLgzJxUR6UntRRVN8JgBMO737ecnbjYodGJJX97achUnW7O/iapB3YDwv3MXwj9CdL6BKnvxzdzTeZly8JI5wAj86tpuxMS7XyV38+geq2V3qxK3xmEtuVyPBzxZsgPOR5S77RbFsp7my7EFUYjuvQOVB0HRUbVaIIzaPu2VmRV65jXNaNIQ0LjertHI14GGs084M21w05+LByg5kMcm9xNrPC63OWpgKOGYbm8lICDWz5X7XkMsaBvbZwtXriOstIyOXslsyqXsenjfR4Af+a0AD0EwGmj8cYbrdjyQX28gDIGZgjkhU3nDapaDmanOVpfB14i3UK33ucwmcLvN0nvUzMJU9/E5--Uc6odNOssdbCwm6d--dJP2D3Hakf5LEn78EPoqeQ==
--------------------------------------------------------------------------------
/lib/rspec/openapi/shared_hooks.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SharedHooks
4 | def self.find_extractor
5 | if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
6 | RSpec::OpenAPI::Extractors::Rails
7 | elsif defined?(Hanami) && Hanami.respond_to?(:app) && Hanami.app?
8 | RSpec::OpenAPI::Extractors::Hanami
9 | # elsif defined?(Roda)
10 | # some Roda extractor
11 | else
12 | RSpec::OpenAPI::Extractors::Rack
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/tables/destroy.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Tables
6 | class Destroy < TableAction
7 | def handle(request, response)
8 | response.format = :json
9 | if request.params[:no_content]
10 | response.status = 202
11 | else
12 | response.body = find_table(request.params[:id]).to_json
13 | end
14 | end
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/sites/site_repository.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Sites
6 | module SiteRepository
7 | class RecordNotFound < StandardError; end
8 |
9 | def find_site(name = nil)
10 | case name
11 | when 'abc123', nil
12 | {
13 | name: name,
14 | }
15 | else
16 | raise RecordNotFound
17 | end
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/images/index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Images
6 | class Index < HanamiTest::Action
7 | format :json
8 |
9 | def handle(_request, response)
10 | list = [
11 | {
12 | name: 'file.png',
13 | tags: [], # Keep this empty to check empty array is accepted
14 | },
15 | ]
16 |
17 | response.body = list.to_json
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/sites/site_action.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Sites
6 | class SiteAction < HanamiTest::Action
7 | include SiteRepository
8 |
9 | handle_exception RecordNotFound => :handle_not_fount_error
10 |
11 | private
12 |
13 | def handle_not_fount_error(_request, response, _exception)
14 | response.status = 404
15 | response.body = { message: 'not found' }.to_json
16 | end
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/array_hashes/single_item.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module ArrayHashes
6 | class SingleItem < HanamiTest::Action
7 | def handle(request, response)
8 | response.format = :json
9 |
10 | response.body = {
11 | "items" => [
12 | {
13 | "id" => 1,
14 | "name" => "Item 1"
15 | }
16 | ]
17 | }.to_json
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/apps/hanami/lib/rack_test/app.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module RackTest
4 | class App
5 | def initialize(app)
6 | @app = app
7 | end
8 |
9 | def call(env)
10 | req = Rack::Request.new(env)
11 | path = req.path_info
12 |
13 | case path
14 | when "/rack/foo"
15 | [200, { 'Content-Type' => 'text/plain' }, ['A RACK FOO']]
16 | when "/rack/bar"
17 | [200, { 'Content-Type' => 'text/plain' }, ['A RACK BAR']]
18 | else
19 | return @app.call(env)
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/.github/release.yaml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | labels:
4 | - ignore-for-release
5 | authors:
6 | - octocat
7 | categories:
8 | - title: 🛠 Breaking Changes
9 | labels:
10 | - semver-major
11 | - breaking-change
12 | - title: 🎉 Exciting New Features
13 | labels:
14 | - semver-minor
15 | - enhancement
16 | - title: 🐞 Bugfixes
17 | labels:
18 | - bug
19 | - title: 📄 Documentation
20 | labels:
21 | - documentation
22 | - title: 📦 Other Changes
23 | labels:
24 | - "*"
25 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/default_schema.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class << RSpec::OpenAPI::DefaultSchema = Object.new
4 | def build(title)
5 | spec = {
6 | openapi: '3.0.3',
7 | info: {
8 | title: title,
9 | version: RSpec::OpenAPI.application_version,
10 | },
11 | servers: RSpec::OpenAPI.servers,
12 | paths: {},
13 | }
14 |
15 | if RSpec::OpenAPI.security_schemes.present?
16 | spec[:components] = {
17 | securitySchemes: RSpec::OpenAPI.security_schemes,
18 | }
19 | end
20 |
21 | spec.freeze
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/key_transformer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class << RSpec::OpenAPI::KeyTransformer = Object.new
4 | def symbolize(value)
5 | case value
6 | when Hash
7 | value.to_h { |k, v| [k.to_sym, symbolize(v)] }
8 | when Array
9 | value.map { |v| symbolize(v) }
10 | else
11 | value
12 | end
13 | end
14 |
15 | def stringify(value)
16 | case value
17 | when Hash
18 | value.to_h { |k, v| [k.to_s, stringify(v)] }
19 | when Array
20 | value.map { |v| stringify(v) }
21 | else
22 | value
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/users/user_action.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Tables
6 | class UserAction < HanamiTest::Action
7 | include UserRepository
8 |
9 | handle_exception RecordNotFound => :handle_not_fount_error
10 |
11 | before :authenticate
12 |
13 | private
14 |
15 | def handle_not_fount_error(_request, response, _exception)
16 | response.status = 404
17 | response.body = { message: 'not found' }.to_json
18 | end
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/tables/index.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Tables
6 | class Index < TableAction
7 | def handle(request, response)
8 | response.headers['X-Cursor'] = 100
9 |
10 | response.format = :json
11 |
12 | response.body = if request.params[:show_columns]
13 | [find_table('42')].to_json
14 | else
15 | [find_table].to_json
16 | end
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/tables/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Tables
6 | class Create < TableAction
7 | format :json
8 |
9 | def handle(request, response)
10 | if request.params[:name].blank? || request.params[:name] == 'some_invalid_name'
11 | response.status = 422
12 | response.body = { error: 'invalid name parameter' }.to_json
13 | else
14 | response.status = 201
15 | response.body = find_table.to_json
16 | end
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/array_hashes/non_nullable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module ArrayHashes
6 | class NonNullable < HanamiTest::Action
7 | def handle(request, response)
8 | response.format = :json
9 |
10 | response.body = {"users" => [
11 | {
12 | "label" => "Jane Doe",
13 | "value" => "jane_doe"
14 | },
15 | {
16 | "label" => "John Doe",
17 | "value" => "john_doe",
18 | }
19 | ]}.to_json
20 | end
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/users/create.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Users
6 | class Create < UserAction
7 | format :json
8 |
9 | def handle(_request, response)
10 | res = {
11 | name: params[:name],
12 | relations: {
13 | avatar: {
14 | url: params[:avatar_url] || 'https://example.com/avatar.png',
15 | },
16 | pets: params[:pets] || [],
17 | },
18 | }
19 |
20 | response.status = 201
21 | response.body = res.to_json
22 | end
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/images/show.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Images
6 | class Show < HanamiTest::Action
7 | def handle(_request, response)
8 | png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjPQBoAAAAASUVORK5CYII='
9 | .unpack('m').first
10 |
11 | response.format = :png
12 | response.body = png
13 | response.headers.merge!(
14 | {
15 | 'Content-Type' => 'image/png',
16 | 'Content-Disposition' => 'inline',
17 | }
18 | )
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_from: .rubocop_todo.yml
2 |
3 | AllCops:
4 | NewCops: enable
5 | SuggestExtensions: false
6 | TargetRubyVersion: 2.7
7 | Exclude:
8 | - 'spec/apps/**/*'
9 | - 'vendor/**/*'
10 |
11 | Style/TrailingCommaInHashLiteral:
12 | EnforcedStyleForMultiline: consistent_comma
13 | Style/TrailingCommaInArguments:
14 | EnforcedStyleForMultiline: consistent_comma
15 | Style/TrailingCommaInArrayLiteral:
16 | EnforcedStyleForMultiline: consistent_comma
17 | Style/ClassAndModuleChildren:
18 | EnforcedStyle: compact
19 | Exclude:
20 | - 'lib/rspec/openapi/version.rb'
21 | Layout/FirstArrayElementIndentation:
22 | EnforcedStyle: consistent
23 | Metrics/BlockLength:
24 | Exclude:
25 | - 'spec/**/*'
26 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/images/upload_nested.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Images
6 | class UploadNested < HanamiTest::Action
7 | def handle(_request, response)
8 | png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjPQBoAAAAASUVORK5CYII='
9 | .unpack('m').first
10 |
11 | response.format = :png
12 | response.body = png
13 | response.headers.merge!(
14 | {
15 | 'Content-Type' => 'image/png',
16 | 'Content-Disposition' => 'inline',
17 | }
18 | )
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/images/upload_multiple_nested.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Images
6 | class UploadMultipleNested < HanamiTest::Action
7 | def handle(_request, response)
8 | png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjPQBoAAAAASUVORK5CYII='
9 | .unpack('m').first
10 |
11 | response.format = :png
12 | response.body = png
13 | response.headers.merge!(
14 | {
15 | 'Content-Type' => 'image/png',
16 | 'Content-Disposition' => 'inline',
17 | }
18 | )
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/images/upload.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Images
6 | class Upload < HanamiTest::Action
7 | # format :form
8 |
9 | def handle(_request, response)
10 | png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjPQBoAAAAASUVORK5CYII='
11 | .unpack('m').first
12 |
13 | response.format = :png
14 | response.body = png
15 | response.headers.merge!(
16 | {
17 | 'Content-Type' => 'image/png',
18 | 'Content-Disposition' => 'inline',
19 | }
20 | )
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/images/upload_multiple.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Images
6 | class UploadMultiple < HanamiTest::Action
7 | # format :multipart
8 |
9 | def handle(_request, response)
10 | png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjPQBoAAAAASUVORK5CYII='
11 | .unpack('m').first
12 |
13 | response.format = :png
14 | response.body = png
15 | response.headers.merge!(
16 | {
17 | 'Content-Type' => 'image/png',
18 | 'Content-Disposition' => 'inline',
19 | }
20 | )
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
6 | * vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/javascript/packs/application.js:
--------------------------------------------------------------------------------
1 | // This file is automatically compiled by Webpack, along with any other files
2 | // present in this directory. You're encouraged to place your actual application logic in
3 | // a relevant structure within app/javascript and only use these pack files to reference
4 | // that code so it'll be compiled.
5 |
6 | require("@rails/ujs").start()
7 | require("@rails/activestorage").start()
8 | require("channels")
9 |
10 |
11 | // Uncomment to copy all static images under ../images to the output folder and reference
12 | // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
13 | // or the `imagePath` JavaScript helper below.
14 | //
15 | // const images = require.context('../images', true)
16 | // const imagePath = (name) => images(name, true)
17 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/array_hashes/nested_arrays.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module ArrayHashes
6 | class NestedArrays < HanamiTest::Action
7 | def handle(request, response)
8 | response.format = :json
9 |
10 | response.body = {
11 | "items" => [
12 | {
13 | "id" => 1,
14 | "tags" => ["ruby", "rails"]
15 | },
16 | {
17 | "id" => 2,
18 | "tags" => ["python", "django"]
19 | },
20 | {
21 | "id" => 3,
22 | "tags" => ["javascript"]
23 | }
24 | ]
25 | }.to_json
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/array_hashes/nullable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module ArrayHashes
6 | class Nullable < HanamiTest::Action
7 | def handle(request, response)
8 | response.format = :json
9 |
10 | response.body = {"users" => [
11 | {
12 | "label" => "John Doe",
13 | "value" => "john_doe",
14 | "admin" => true
15 | },
16 | {
17 | "label" => "Jane Doe",
18 | "value" => "jane_doe"
19 | },
20 | {
21 | "label" => nil,
22 | "value" => "unknown",
23 | "invited" => true
24 | },
25 | ]}.to_json
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/users/user_repository.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Users
6 | module UserRepository
7 | class RecordNotFound < StandardError; end
8 |
9 | def find_user(id = nil)
10 | case id
11 | when '1', nil
12 | {
13 | name: 'John Doe',
14 | relations: {
15 | avatar: {
16 | url: 'https://example.com/avatar.jpg',
17 | },
18 | pets: [
19 | { name: 'doge', age: 8 },
20 | ],
21 | },
22 | }
23 | when '2'
24 | {}
25 | else
26 | raise RecordNotFound
27 | end
28 | end
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/tables/table_action.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Tables
6 | class TableAction < HanamiTest::Action
7 | APIKEY = 'k0kubun'.freeze
8 |
9 | include TableRepository
10 |
11 | handle_exception RecordNotFound => :handle_not_fount_error
12 |
13 | before :authenticate
14 |
15 | private
16 |
17 | def handle_not_fount_error(_request, _response, _exception)
18 | halt 404, { message: 'not found' }.to_json
19 | end
20 |
21 | def authenticate(request, response)
22 | return unless request.get_header('AUTHORIZATION') != APIKEY
23 |
24 | response.format = :json
25 | halt 401, { message: 'Unauthorized' }.to_json
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/images_controller.rb:
--------------------------------------------------------------------------------
1 | class ImagesController < ApplicationController
2 | def show
3 | send_image
4 | end
5 |
6 | def index
7 | list = [
8 | {
9 | 'name': 'file.png',
10 | 'tags': [], # Keep this empty to check empty array is accepted
11 | },
12 | ]
13 | render json: list
14 | end
15 |
16 | def upload
17 | send_image
18 | end
19 |
20 | def upload_nested
21 | send_image
22 | end
23 |
24 | def upload_multiple
25 | send_image
26 | end
27 |
28 | def upload_multiple_nested
29 | send_image
30 | end
31 |
32 | private
33 |
34 | def send_image
35 | png = 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAAAAADhZOFXAAAADklEQVQIW2P4DwUMlDEA98A/wTjP
36 | QBoAAAAASUVORK5CYII='.unpack('m').first
37 | send_data png, type: 'image/png', disposition: 'inline'
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/.github/workflows/rubocop.yml:
--------------------------------------------------------------------------------
1 | name: "Rubocop"
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | rubocop:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 |
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v6
18 |
19 | - name: Set up Ruby
20 | uses: ruby/setup-ruby@v1
21 | with:
22 | ruby-version: 3.3
23 | bundler-cache: true
24 |
25 | - name: Rubocop run
26 | run: |
27 | bash -c "
28 | bundle exec rubocop --require code_scanning --format CodeScanning::SarifFormatter -o rubocop.sarif
29 | [[ $? -ne 2 ]]
30 | "
31 |
32 | - name: Upload Sarif output
33 | uses: github/codeql-action/upload-sarif@v4
34 | with:
35 | sarif_file: rubocop.sarif
36 |
--------------------------------------------------------------------------------
/spec/integration_tests/roda_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative '../apps/roda/roda_app'
4 | require 'json'
5 | require 'rack/test'
6 | require 'minitest/autorun'
7 | require 'rspec/openapi'
8 |
9 | ENV['OPENAPI_OUTPUT'] ||= 'yaml'
10 |
11 | RSpec::OpenAPI.title = 'OpenAPI Documentation'
12 | RSpec::OpenAPI.path = File.expand_path("../apps/roda/doc/minitest_openapi.#{ENV.fetch('OPENAPI_OUTPUT', nil)}", __dir__)
13 | RSpec::OpenAPI.ignored_paths = ['/admin/masters/extensions']
14 |
15 | class RodaTest < Minitest::Test
16 | include Rack::Test::Methods
17 |
18 | i_suck_and_my_tests_are_order_dependent!
19 | openapi!
20 |
21 | def app
22 | RodaApp
23 | end
24 |
25 | def test_when_id_is_given_it_returns_200
26 | post '/roda', { id: 1 }.to_json, 'CONTENT_TYPE' => 'application/json'
27 | assert_equal 200, last_response.status
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | # Specify your gem's dependencies in rspec-openapi.gemspec
6 | gemspec
7 |
8 | gem 'rails', ENV['RAILS_VERSION'] || '6.0.6.1'
9 |
10 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0')
11 | gem 'hanami', ENV['HANAMI_VERSION'] || '2.1.0'
12 | gem 'hanami-controller', ENV['HANAMI_VERSION'] || '2.1.0'
13 | gem 'hanami-router', ENV['HANAMI_VERSION'] || '2.1.0'
14 |
15 | gem 'dry-logger', '1.0.3'
16 | end
17 |
18 | gem 'concurrent-ruby', '1.3.4'
19 |
20 | gem 'roda'
21 |
22 | gem 'rails-dom-testing', '~> 2.2'
23 | gem 'rspec-rails'
24 |
25 | group :test do
26 | gem 'simplecov', git: 'https://github.com/exoego/simplecov.git', branch: 'branch-fix'
27 | gem 'simplecov-cobertura'
28 | gem 'super_diff'
29 | end
30 |
31 | group :development do
32 | gem 'code-scanning-rubocop'
33 | gem 'pry'
34 | end
35 |
--------------------------------------------------------------------------------
/spec/apps/rails/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 | /db/*.sqlite3-*
14 |
15 | # Ignore all logfiles and tempfiles.
16 | /log/.keep
17 | /tmp/*
18 | !/log/.keep
19 | !/tmp/.keep
20 |
21 | # Ignore pidfiles, but keep the directory.
22 | /tmp/pids/*
23 | !/tmp/pids/
24 | !/tmp/pids/.keep
25 |
26 | # Ignore uploaded files in development.
27 | /storage/*
28 | !/storage/.keep
29 |
30 | /public/assets
31 | .byebug_history
32 |
33 | # Ignore master key for decrypting credentials and more.
34 | /config/master.key
35 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/rspec_hooks.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rspec/core'
4 |
5 | RSpec.configuration.after(:each) do |example|
6 | if RSpec::OpenAPI.example_types.include?(example.metadata[:type]) && example.metadata[:openapi] != false
7 | path = RSpec::OpenAPI.path.then { |p| p.is_a?(Proc) ? p.call(example) : p }
8 | record = RSpec::OpenAPI::RecordBuilder.build(self, example: example, extractor: SharedHooks.find_extractor)
9 | RSpec::OpenAPI.path_records[path] << record if record
10 | end
11 | end
12 |
13 | RSpec.configuration.after(:suite) do
14 | result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
15 | result_recorder.record_results!
16 | if result_recorder.errors?
17 | error_message = result_recorder.error_message
18 | colorizer = RSpec::Core::Formatters::ConsoleCodes
19 | RSpec.configuration.reporter.message colorizer.wrap(error_message, :failure)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [ "master" ]
9 | schedule:
10 | - cron: '20 22 * * 2'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 | permissions:
17 | actions: read
18 | contents: read
19 | security-events: write
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | language: [ 'ruby' ]
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v6
29 |
30 | - name: Initialize CodeQL
31 | uses: github/codeql-action/init@v4
32 | with:
33 | languages: ${{ matrix.language }}
34 |
35 | - name: Autobuild
36 | uses: github/codeql-action/autobuild@v4
37 |
38 | - name: Perform CodeQL Analysis
39 | uses: github/codeql-action/analyze@v4
40 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | def create
3 | res = {
4 | name: params[:name] || 'alice',
5 | relations: {
6 | avatar: {
7 | url: params[:avatar_url] || 'https://example.com/avatar.png',
8 | },
9 | pets: params[:pets] || [],
10 | },
11 | }
12 | render json: res, status: 201
13 | end
14 |
15 | def show
16 | render json: find_user(params[:id])
17 | end
18 |
19 | def active
20 | render json: find_user(params[:id]).present?
21 | end
22 |
23 | private
24 |
25 | def find_user(id = nil)
26 | case id
27 | when '1', nil
28 | {
29 | name: 'John Doe',
30 | relations: {
31 | avatar: {
32 | url: 'https://example.com/avatar.jpg',
33 | },
34 | pets: [
35 | { name: 'doge', age: 8 },
36 | ],
37 | },
38 | }
39 | when '2'
40 | {}
41 | else
42 | raise NotFoundError
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/spec/apps/roda/doc/minitest_openapi.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | openapi: 3.0.3
3 | info:
4 | title: OpenAPI Documentation
5 | version: 7.7.7
6 | servers: []
7 | paths:
8 | "/roda":
9 | post:
10 | summary: POST /roda
11 | requestBody:
12 | content:
13 | application/json:
14 | schema:
15 | type: object
16 | properties:
17 | id:
18 | type: integer
19 | required:
20 | - id
21 | example:
22 | id: 1
23 | responses:
24 | '200':
25 | description: when id is given it returns 200
26 | content:
27 | application/json:
28 | schema:
29 | type: object
30 | properties:
31 | id:
32 | type: integer
33 | name:
34 | type: string
35 | required:
36 | - id
37 | - name
38 | example:
39 | id: 1
40 | name: hello
41 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/schema_sorter.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class << RSpec::OpenAPI::SchemaSorter = Object.new
4 | # Sort some unpredictably ordered properties in a lexicographical manner to make the order more predictable.
5 | #
6 | # @param [Hash|Array]
7 | def deep_sort!(spec)
8 | # paths
9 | deep_sort_by_selector!(spec, 'paths')
10 |
11 | # methods
12 | deep_sort_by_selector!(spec, 'paths.*')
13 |
14 | # response status code
15 | deep_sort_by_selector!(spec, 'paths.*.*.responses')
16 |
17 | # content-type
18 | deep_sort_by_selector!(spec, 'paths.*.*.responses.*.content')
19 | end
20 |
21 | private
22 |
23 | # @param [Hash] base
24 | # @param [String] selector
25 | def deep_sort_by_selector!(base, selector)
26 | RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
27 | deep_sort_hash!(base.dig(*paths))
28 | end
29 | end
30 |
31 | def deep_sort_hash!(hash)
32 | sorted = hash.entries.sort_by { |k, _| k.to_s }.to_h.transform_keys(&:to_sym)
33 | hash.replace(sorted)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/apps/roda/doc/rspec_openapi.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | openapi: 3.0.3
3 | info:
4 | title: OpenAPI Documentation
5 | version: 7.7.7
6 | servers: []
7 | paths:
8 | "/roda":
9 | post:
10 | summary: Create roda resource
11 | requestBody:
12 | content:
13 | application/json:
14 | schema:
15 | type: object
16 | properties:
17 | id:
18 | type: integer
19 | required:
20 | - id
21 | example:
22 | id: 1
23 | responses:
24 | '200':
25 | description: when id is given it returns 200
26 | content:
27 | application/json:
28 | schema:
29 | type: object
30 | properties:
31 | id:
32 | type: integer
33 | name:
34 | type: string
35 | required:
36 | - id
37 | - name
38 | example:
39 | id: 1
40 | name: hello
41 |
--------------------------------------------------------------------------------
/spec/requests/roda_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative '../apps/roda/roda_app'
4 | require 'json'
5 | require 'rack/test'
6 |
7 | ENV['OPENAPI_OUTPUT'] ||= 'yaml'
8 |
9 | RSpec::OpenAPI.title = 'OpenAPI Documentation'
10 | RSpec::OpenAPI.path = File.expand_path("../apps/roda/doc/rspec_openapi.#{ENV.fetch('OPENAPI_OUTPUT', nil)}", __dir__)
11 | RSpec::OpenAPI.ignored_paths = ['/admin/masters/extensions']
12 |
13 | RSpec::OpenAPI.description_builder = lambda do |example|
14 | contexts = example.example_group.parent_groups.map(&:description).grep(/\Awhen /)
15 | [*contexts, 'it', example.description].join(' ')
16 | end
17 |
18 | RSpec.describe 'Roda', type: :request do
19 | include Rack::Test::Methods
20 |
21 | let(:app) do
22 | RodaApp
23 | end
24 |
25 | describe '/roda', openapi: { summary: 'Create roda resource' } do
26 | context 'when id is given' do
27 | it 'returns 200' do
28 | post '/roda', { id: 1 }.to_json, 'CONTENT_TYPE' => 'application/json'
29 | expect(last_response.status).to eq(200)
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Takashi Kokubun
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 |
--------------------------------------------------------------------------------
/spec/rspec/rack_test_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 | require 'fileutils'
5 | require 'yaml'
6 |
7 | RSpec.describe 'rack-test spec' do
8 | include SpecHelper
9 |
10 | describe 'yaml output' do
11 | let(:openapi_path) do
12 | File.expand_path('spec/apps/roda/doc/rspec_openapi.yaml', repo_root)
13 | end
14 |
15 | it 'generates the same spec/apps/roda/doc/rspec_openapi.yaml' do
16 | org_yaml = YAML.safe_load(File.read(openapi_path))
17 | rspec 'spec/requests/roda_spec.rb', openapi: true
18 | new_yaml = YAML.safe_load(File.read(openapi_path))
19 | expect(new_yaml).to eq org_yaml
20 | end
21 | end
22 |
23 | describe 'json output' do
24 | let(:openapi_path) do
25 | File.expand_path('spec/apps/roda/doc/rspec_openapi.json', repo_root)
26 | end
27 |
28 | it 'generates the same spec/apps/roda/doc/rspec_openapi.json' do
29 | org_yaml = YAML.safe_load(File.read(openapi_path))
30 | rspec 'spec/requests/roda_spec.rb', openapi: true, output: :json
31 | new_yaml = YAML.safe_load(File.read(openapi_path))
32 | expect(new_yaml).to eq org_yaml
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/spec/apps/rails/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'fileutils'
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path('..', __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to setup or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies
21 | # system('bin/yarn')
22 |
23 | # puts "\n== Copying sample files =="
24 | # unless File.exist?('config/database.yml')
25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
26 | # end
27 |
28 | puts "\n== Preparing database =="
29 | system! 'bin/rails db:prepare'
30 |
31 | puts "\n== Removing old logs and tempfiles =="
32 | system! 'bin/rails log:clear tmp:clear'
33 |
34 | puts "\n== Restarting application server =="
35 | system! 'bin/rails restart'
36 | end
37 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/spec/rspec/hanami_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | return unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0')
4 |
5 | require 'spec_helper'
6 | require 'yaml'
7 | require 'json'
8 | require 'pry'
9 |
10 | RSpec.describe 'hanami request spec' do
11 | include SpecHelper
12 |
13 | describe 'yaml output' do
14 | let(:openapi_path) do
15 | File.expand_path('spec/apps/hanami/doc/openapi.yaml', repo_root)
16 | end
17 |
18 | it 'generates the same spec/apps/hanami/doc/openapi.yaml' do
19 | org_yaml = YAML.safe_load(File.read(openapi_path))
20 | rspec 'spec/requests/hanami_spec.rb', openapi: true, output: :yaml
21 | new_yaml = YAML.safe_load(File.read(openapi_path))
22 | expect(new_yaml).to eq org_yaml
23 | end
24 | end
25 |
26 | describe 'json' do
27 | let(:openapi_path) do
28 | File.expand_path('spec/apps/hanami/doc/openapi.json', repo_root)
29 | end
30 |
31 | it 'generates the same spec/apps/hanami/doc/openapi.json' do
32 | org_json = JSON.parse(File.read(openapi_path))
33 | rspec 'spec/requests/hanami_spec.rb', openapi: true, output: :json
34 | new_json = JSON.parse(File.read(openapi_path))
35 | expect(new_json).to eq org_json
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/requests/rspec_hook_error_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ENV['TZ'] ||= 'UTC'
4 | ENV['RAILS_ENV'] ||= 'test'
5 | ENV['OPENAPI_OUTPUT'] ||= 'yaml'
6 |
7 | require File.expand_path('../apps/rails/config/environment', __dir__)
8 | require 'rspec/rails'
9 | require 'fileutils'
10 |
11 | openapi_path = ENV.fetch('RSPEC_HOOK_OPENAPI_PATH') do
12 | File.expand_path('../apps/rails/tmp/rspec_hook_error.yaml', __dir__)
13 | end
14 | FileUtils.mkdir_p(File.dirname(openapi_path))
15 | FileUtils.rm_f(openapi_path)
16 |
17 | RSpec::OpenAPI.title = 'OpenAPI Documentation'
18 | RSpec::OpenAPI.path = openapi_path
19 | RSpec::OpenAPI.request_headers = []
20 | RSpec::OpenAPI.response_headers = []
21 | RSpec::OpenAPI.path_records.clear
22 |
23 | RSpec.describe 'RSpec hooks error handling', type: :request do
24 | after(:context) do
25 | path = RSpec::OpenAPI.path
26 | record = RSpec::OpenAPI.path_records[path].last
27 | raise 'OpenAPI record was not generated' unless record
28 |
29 | invalid_record = RSpec::OpenAPI::Record.new(**record.to_h.merge(response_body: Object.new))
30 | RSpec::OpenAPI.path_records[path] << invalid_record
31 | end
32 |
33 | it 'produces reporter output when schema building fails' do
34 | get '/invalid_responses'
35 | expect(response).to have_http_status(:ok)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require "rails"
4 | # Pick the frameworks you want:
5 | # require "active_model/railtie"
6 | # require "active_job/railtie"
7 | # require "active_record/railtie"
8 | # require "active_storage/engine"
9 | require "action_controller/railtie"
10 | # require "action_mailer/railtie"
11 | # require "action_mailbox/engine"
12 | # require "action_text/engine"
13 | require "action_view/railtie"
14 | # require "action_cable/engine"
15 | # require "sprockets/railtie"
16 | # require "rails/test_unit/railtie"
17 |
18 | require_relative "../vendor/my_engine/lib/my_engine/engine"
19 |
20 | # Require the gems listed in Gemfile, including any gems
21 | # you've limited to :test, :development, or :production.
22 | Bundler.require(*Rails.groups)
23 |
24 | module Railsapp
25 | class Application < Rails::Application
26 | # Initialize configuration defaults for originally generated Rails version.
27 | config.load_defaults 6.0
28 |
29 | # Settings in config/environments/* take precedence over those specified here.
30 | # Application configuration can go into files in config/initializers
31 | # -- all .rb files in that directory are automatically loaded after loading
32 | # the framework and any gems in your application.
33 |
34 | # Don't generate system test files.
35 | config.generators.system_tests = nil
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/minitest/rack_test_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 | require 'fileutils'
5 | require 'yaml'
6 |
7 | RSpec.describe 'rack-test minitest' do
8 | include SpecHelper
9 |
10 | describe 'yaml output' do
11 | let(:openapi_path) do
12 | File.expand_path('spec/apps/roda/doc/minitest_openapi.yaml', repo_root)
13 | end
14 |
15 | it 'generates the same spec/apps/roda/doc/minitest_openapi.yaml' do
16 | org_yaml = YAML.safe_load(File.read(openapi_path))
17 | minitest 'spec/integration_tests/roda_test.rb', openapi: true
18 | new_yaml = YAML.safe_load(File.read(openapi_path))
19 | expect(new_yaml).to eq org_yaml
20 | end
21 | end
22 |
23 | describe 'json output' do
24 | let(:openapi_path) do
25 | File.expand_path('spec/apps/roda/doc/minitest_openapi.json', repo_root)
26 | end
27 |
28 | it 'generates the same spec/apps/roda/doc/minitest_openapi.json' do
29 | org_yaml = YAML.safe_load(File.read(openapi_path))
30 | minitest 'spec/integration_tests/roda_test.rb', openapi: true, output: :json
31 | new_yaml = YAML.safe_load(File.read(openapi_path))
32 | expect(new_yaml).to eq org_yaml
33 | end
34 | end
35 |
36 | describe 'with disabled OpenAPI generation' do
37 | it 'can run tests' do
38 | minitest 'spec/integration_tests/roda_test.rb'
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/rspec/schema_file_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe RSpec::OpenAPI::SchemaFile do
6 | describe '#read' do
7 | let(:schema_content) do
8 | <<~YAML
9 | openapi: 3.0.0
10 | info:
11 | title: My API
12 | version: 1.0.0
13 | paths:
14 | /:
15 | get:
16 | summary: A test endpoint
17 | parameters:
18 | - name: date
19 | in: query
20 | schema:
21 | type: string
22 | date: 2020-01-02 # Unquoted date
23 | time: 2025-06-10 01:47:28Z
24 | YAML
25 | end
26 |
27 | it 'deserializes unquoted dates as Date objects when Date is permitted' do
28 | schema_file = RSpec::OpenAPI::SchemaFile.new('nonexistant/schema.yaml')
29 |
30 | expect(File).to receive(:read).and_return(schema_content)
31 | expect(File).to receive(:exist?).and_return(true)
32 |
33 | data = nil
34 | expect do
35 | data = schema_file.send(:read)
36 | end.not_to raise_error(Psych::DisallowedClass)
37 | expect(data.dig(:paths, :/, :get, :parameters, 0, :schema, :date).to_s).to eq('2020-01-02')
38 | expect(data.dig(:paths, :/, :get, :parameters, 0, :schema, :time).to_s).to eq('2025-06-10 01:47:28 UTC')
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish to RubyGems
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | publish:
10 | name: Publish gem and GitHub release
11 | runs-on: ubuntu-latest
12 |
13 | permissions:
14 | id-token: write # for RubyGems trusted publishing
15 | contents: write # to create GitHub release
16 |
17 | steps:
18 | - uses: actions/checkout@v5
19 | with:
20 | fetch-depth: 0
21 |
22 | - name: Set up Ruby
23 | uses: ruby/setup-ruby@v1
24 | with:
25 | bundler-cache: true
26 | ruby-version: ruby
27 |
28 | - name: Verify tag matches version.rb
29 | run: |
30 | set -euo pipefail
31 | tag="${GITHUB_REF_NAME}"
32 | version="${tag#v}"
33 | file_version=$(ruby -e "require_relative './lib/rspec/openapi/version'; puts RSpec::OpenAPI::VERSION")
34 | if [ "$version" != "$file_version" ]; then
35 | echo "Tag version ($version) does not match lib/rspec/openapi/version.rb ($file_version)" >&2
36 | exit 1
37 | fi
38 |
39 | - uses: rubygems/release-gem@v1
40 |
41 | - name: Create GitHub release
42 | uses: softprops/action-gh-release@v2
43 | with:
44 | tag_name: ${{ github.ref_name }}
45 | name: ${{ github.ref_name }}
46 | generate_release_notes: true
47 |
--------------------------------------------------------------------------------
/spec/minitest/rails_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 | require 'yaml'
5 | require 'json'
6 | require 'pry'
7 |
8 | RSpec.describe 'rails integration minitest' do
9 | include SpecHelper
10 |
11 | describe 'yaml output' do
12 | let(:openapi_path) do
13 | File.expand_path('spec/apps/rails/doc/minitest_openapi.yaml', repo_root)
14 | end
15 |
16 | it 'generates the same spec/apps/rails/doc/minitest_openapi.yaml' do
17 | org_yaml = YAML.safe_load(File.read(openapi_path))
18 | minitest 'spec/integration_tests/rails_test.rb', openapi: true, output: :yaml
19 | new_yaml = YAML.safe_load(File.read(openapi_path))
20 | expect(new_yaml).to eq org_yaml
21 | end
22 | end
23 |
24 | describe 'json' do
25 | let(:openapi_path) do
26 | File.expand_path('spec/apps/rails/doc/minitest_openapi.json', repo_root)
27 | end
28 |
29 | it 'generates the same spec/apps/rails/doc/minitest_openapi.json' do
30 | org_yaml = JSON.parse(File.read(openapi_path))
31 | minitest 'spec/integration_tests/rails_test.rb', openapi: true, output: :json
32 | new_yaml = JSON.parse(File.read(openapi_path))
33 | expect(new_yaml).to eq org_yaml
34 | end
35 | end
36 |
37 | describe 'with disabled OpenAPI generation' do
38 | it 'can run tests' do
39 | minitest 'spec/integration_tests/rails_test.rb'
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/rspec/scheme_merger_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | RSpec.describe 'schema merger spec' do
6 | include SpecHelper
7 |
8 | describe 'mixed symbol and strings' do
9 | let(:base) do
10 | {
11 | 'n' => 1,
12 | 'required' => %w[foo bar],
13 | 'a' => {
14 | b1: 1,
15 | b2: %w[foo bar],
16 | 'b3' => {
17 | 'c1' => 2,
18 | c2: 3,
19 | },
20 | },
21 | }
22 | end
23 |
24 | let(:spec) do
25 | {
26 | n: 1,
27 | required: ['buz'],
28 | a: {
29 | 'b1' => 1,
30 | 'b2' => %w[foo bar],
31 | b3: {
32 | c1: 2,
33 | 'c2' => 3,
34 | },
35 | },
36 | }
37 | end
38 |
39 | it 'normalize keys to symbol' do
40 | result = RSpec::OpenAPI::SchemaMerger.merge!(base, spec)
41 | expect(result).to eq({
42 | n: 1,
43 | required: [],
44 | a: {
45 | b1: 1,
46 | b2: %w[foo bar],
47 | b3: {
48 | c1: 2,
49 | c2: 3,
50 | },
51 | },
52 | })
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/schema_file.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'fileutils'
4 | require 'yaml'
5 | require 'json'
6 |
7 | # For Ruby 2.7
8 | require 'date'
9 |
10 | # TODO: Support JSON
11 | class RSpec::OpenAPI::SchemaFile
12 | # @param [String] path
13 | def initialize(path)
14 | @path = path
15 | end
16 |
17 | def edit(&block)
18 | spec = read
19 | block.call(spec)
20 | ensure
21 | write(RSpec::OpenAPI::KeyTransformer.stringify(spec))
22 | end
23 |
24 | private
25 |
26 | # @return [Hash]
27 | def read
28 | return {} unless File.exist?(@path)
29 |
30 | RSpec::OpenAPI::KeyTransformer.symbolize(
31 | YAML.safe_load(
32 | File.read(@path),
33 | permitted_classes: [Date, Time],
34 | ),
35 | ) # this can also parse JSON
36 | end
37 |
38 | # @param [Hash] spec
39 | def write(spec)
40 | FileUtils.mkdir_p(File.dirname(@path))
41 | output =
42 | if json?
43 | JSON.pretty_generate(spec)
44 | else
45 | prepend_comment(YAML.dump(spec))
46 | end
47 | File.write(@path, output)
48 | end
49 |
50 | def prepend_comment(content)
51 | return content if RSpec::OpenAPI.comment.nil?
52 |
53 | comment = RSpec::OpenAPI.comment.dup
54 | comment << "\n" unless comment.end_with?("\n")
55 | "#{comment.gsub(/^/, '# ').gsub(/^# \n/, "#\n")}#{content}"
56 | end
57 |
58 | def json?
59 | File.extname(@path) == '.json'
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | types:
8 | - opened
9 | - synchronize
10 | - reopened
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 | container: ${{ matrix.ruby }}
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | include:
19 | - ruby: ruby:2.7
20 | - ruby: ruby:3.0
21 | - ruby: ruby:3.1
22 | - ruby: ruby:3.1
23 | rails: 6.1.7
24 | - ruby: ruby:3.1
25 | rails: 7.0.8
26 | - ruby: ruby:3.3
27 | rails: 7.1.3.2
28 | - ruby: ruby:3.4
29 | rails: 8.0.2
30 | coverage: coverage
31 | env:
32 | RAILS_VERSION: ${{ matrix.rails == '' && '6.1.6' || matrix.rails }}
33 | COVERAGE: ${{ matrix.coverage || '' }}
34 | steps:
35 | - uses: actions/checkout@v6
36 | - name: bundle install
37 | run: bundle install -j$(nproc) --retry 3
38 | - run: bundle exec rspec
39 | timeout-minutes: 1
40 | - run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
41 | name: codecov-action@v4 workaround
42 | - name: Upload coverage reports
43 | uses: codecov/codecov-action@v5
44 | if: matrix.coverage == 'coverage'
45 | with:
46 | fail_ci_if_error: true
47 | files: ./coverage/coverage.xml
48 | token: ${{ secrets.CODECOV_TOKEN }}
49 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/array_hashes/nested.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module ArrayHashes
6 | class Nested < HanamiTest::Action
7 | def handle(request, response)
8 | response.format = :json
9 |
10 | response.body = {
11 | "fields" => [
12 | {
13 | "id" => "country_code",
14 | "options" => [
15 | {
16 | "id" => "us",
17 | "label" => "United States"
18 | },
19 | {
20 | "id" => "ca",
21 | "label" => "Canada"
22 | }
23 | ],
24 | "validations" => nil,
25 | "always_nil" => nil
26 | },
27 | {
28 | "id" => "region_id",
29 | "options" => [
30 | {
31 | "id" => 1,
32 | "label" => "New York"
33 | },
34 | {
35 | "id" => 2,
36 | "label" => "California"
37 | }
38 | ],
39 | "validations" => {
40 | "presence" => true
41 | },
42 | "always_nil" => nil
43 | }
44 | ]
45 | }.to_json
46 | end
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/apps/hanami/config/puma.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | #
4 | # Environment and port
5 | #
6 | port ENV.fetch('HANAMI_PORT', 2300)
7 | environment ENV.fetch('HANAMI_ENV', 'development')
8 |
9 | #
10 | # Threads within each Puma/Ruby process (aka worker)
11 | #
12 |
13 | # Configure the minimum and maximum number of threads to use to answer requests.
14 | max_threads_count = ENV.fetch('HANAMI_MAX_THREADS', 5)
15 | min_threads_count = ENV.fetch('HANAMI_MIN_THREADS') { max_threads_count }
16 |
17 | threads min_threads_count, max_threads_count
18 |
19 | #
20 | # Workers (aka Puma/Ruby processes)
21 | #
22 |
23 | puma_concurrency = Integer(ENV.fetch('HANAMI_WEB_CONCURRENCY', 0))
24 | puma_cluster_mode = puma_concurrency > 1
25 |
26 | # How many worker (Puma/Ruby) processes to run.
27 | # Typically this is set to the number of available cores.
28 | workers puma_concurrency
29 |
30 | #
31 | # Cluster mode (aka multiple workers)
32 | #
33 |
34 | if puma_cluster_mode
35 | # Preload the application before starting the workers. Only in cluster mode.
36 | preload_app!
37 |
38 | # Code to run immediately before master process forks workers (once on boot).
39 | #
40 | # These hooks can block if necessary to wait for background operations unknown
41 | # to puma to finish before the process terminates. This can be used to close
42 | # any connections to remote servers (database, redis, …) that were opened when
43 | # preloading the code.
44 | before_fork do
45 | Hanami.shutdown
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/hash_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class << RSpec::OpenAPI::HashHelper = Object.new
4 | def paths_to_all_fields(obj)
5 | case obj
6 | when Hash
7 | obj.each.flat_map do |k, v|
8 | k = k.to_sym
9 | [[k]] + paths_to_all_fields(v).map { |x| [k, *x] }
10 | end
11 | when Array
12 | obj.flat_map.with_index do |value, i|
13 | [[i]] + paths_to_all_fields(value).map { |x| [i, *x] }
14 | end
15 | else
16 | []
17 | end
18 | end
19 |
20 | def matched_paths(obj, selector)
21 | selector_parts = selector.split('.').map(&:to_sym)
22 | paths_to_all_fields(obj).select do |key_parts|
23 | key_parts.size == selector_parts.size && key_parts.zip(selector_parts).all? do |kp, sp|
24 | kp == sp || (sp == :* && !kp.nil?)
25 | end
26 | end
27 | end
28 |
29 | def matched_paths_deeply_nested(obj, begin_selector, end_selector)
30 | path_depth_sizes = paths_to_all_fields(obj).map(&:size).uniq
31 | path_depth_sizes.map do |depth|
32 | begin_selector_count = begin_selector.is_a?(Symbol) ? 0 : begin_selector.count('.')
33 | end_selector_count = end_selector.is_a?(Symbol) ? 0 : end_selector.count('.')
34 | diff = depth - begin_selector_count - end_selector_count
35 | if diff >= 0
36 | selector = "#{begin_selector}.#{'*.' * diff}#{end_selector}"
37 | matched_paths(obj, selector)
38 | else
39 | []
40 | end
41 | end.flatten(1)
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/rspec-openapi.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/rspec/openapi/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'rspec-openapi'
7 | spec.version = RSpec::OpenAPI::VERSION
8 | spec.authors = ['Takashi Kokubun', 'TATSUNO Yasuhiro']
9 | spec.email = ['takashikkbn@gmail.com', 'ytatsuno.jp@gmail.com']
10 |
11 | spec.summary = 'Generate OpenAPI schema from RSpec request specs'
12 | spec.description = 'Generate OpenAPI from RSpec request specs'
13 | spec.homepage = 'https://github.com/exoego/rspec-openapi'
14 | spec.license = 'MIT'
15 | spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
16 |
17 | spec.metadata = {
18 | 'homepage_uri' => 'https://github.com/exoego/rspec-openapi',
19 | 'source_code_uri' => 'https://github.com/exoego/rspec-openapi',
20 | 'changelog_uri' => "https://github.com/exoego/rspec-openapi/releases/tag/v#{RSpec::OpenAPI::VERSION}",
21 | 'rubygems_mfa_required' => 'true',
22 | }
23 |
24 | spec.files = Dir.chdir(File.expand_path(__dir__)) do
25 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26 | end
27 | spec.bindir = 'exe'
28 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29 | spec.require_paths = ['lib']
30 |
31 | spec.add_dependency 'actionpack', '>= 5.2.0'
32 | spec.add_dependency 'rails-dom-testing'
33 | spec.add_dependency 'rspec-core'
34 | spec.metadata['rubygems_mfa_required'] = 'true'
35 | end
36 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/tables/table_repository.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module Tables
6 | module TableRepository
7 | class RecordNotFound < StandardError; end
8 |
9 | def find_table(id = nil)
10 | time = Time.parse('2020-07-17 00:00:00')
11 | case id
12 | when '1', nil
13 | {
14 | id: 1,
15 | name: 'access',
16 | description: 'logs',
17 | database: {
18 | id: 2,
19 | name: 'production',
20 | },
21 | null_sample: nil,
22 | storage_size: 12.3,
23 | created_at: time.iso8601,
24 | updated_at: time.iso8601,
25 | }
26 | when '42'
27 | {
28 | id: 42,
29 | name: 'access',
30 | description: 'logs',
31 | database: {
32 | id: 4242,
33 | name: 'production',
34 | },
35 | columns: [
36 | { name: 'id', column_type: 'integer' },
37 | { name: 'description', column_type: 'varchar' },
38 | ],
39 | null_sample: nil,
40 | storage_size: 12.3,
41 | created_at: time.iso8601,
42 | updated_at: time.iso8601,
43 | }
44 | else
45 | raise RecordNotFound
46 | end
47 | end
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy
4 | # For further information see the following documentation
5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6 |
7 | # Rails.application.config.content_security_policy do |policy|
8 | # policy.default_src :self, :https
9 | # policy.font_src :self, :https, :data
10 | # policy.img_src :self, :https, :data
11 | # policy.object_src :none
12 | # policy.script_src :self, :https
13 | # policy.style_src :self, :https
14 | # # If you are using webpack-dev-server then specify webpack-dev-server host
15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
16 |
17 | # # Specify URI for violation reports
18 | # # policy.report_uri "/csp-violation-report-endpoint"
19 | # end
20 |
21 | # If you are using UJS then enable automatic nonce generation
22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
23 |
24 | # Set the nonce only to specific directives
25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
26 |
27 | # Report CSP violations to a specified URI
28 | # For further information see the following documentation:
29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
30 | # Rails.application.config.content_security_policy_report_only = true
31 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/minitest_hooks.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'minitest'
4 |
5 | module RSpec::OpenAPI::Minitest
6 | Example = Struct.new(:context, :description, :metadata, :file_path)
7 |
8 | module RunPatch
9 | def run(*args)
10 | result = super
11 | if ENV['OPENAPI'] && self.class.openapi?
12 | file_path = method(name).source_location.first
13 | human_name = name.sub(/^test_/, '').gsub('_', ' ')
14 | example = Example.new(self, human_name, {}, file_path)
15 | path = RSpec::OpenAPI.path.then { |p| p.is_a?(Proc) ? p.call(example) : p }
16 | record = RSpec::OpenAPI::RecordBuilder.build(self, example: example, extractor: SharedHooks.find_extractor)
17 | RSpec::OpenAPI.path_records[path] << record if record
18 | end
19 | result
20 | end
21 | end
22 |
23 | module ActivateOpenApiClassMethods
24 | def self.prepended(base)
25 | base.extend(ClassMethods)
26 | end
27 |
28 | module ClassMethods
29 | def openapi?
30 | @openapi
31 | end
32 |
33 | def openapi!
34 | @openapi = true
35 | end
36 | end
37 | end
38 | end
39 |
40 | Minitest::Test.prepend RSpec::OpenAPI::Minitest::ActivateOpenApiClassMethods
41 |
42 | if ENV['OPENAPI']
43 | Minitest::Test.prepend RSpec::OpenAPI::Minitest::RunPatch
44 |
45 | Minitest.after_run do
46 | result_recorder = RSpec::OpenAPI::ResultRecorder.new(RSpec::OpenAPI.path_records)
47 | result_recorder.record_results!
48 | puts result_recorder.error_message if result_recorder.errors?
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'open3'
4 | require 'super_diff/rspec'
5 |
6 | module SpecHelper
7 | def repo_root
8 | File.expand_path('..', __dir__)
9 | end
10 |
11 | def assert_run(*args)
12 | out, err, status = Open3.capture3(*args)
13 | expect(status.success?).to eq(true), "stdout:\n#{out}\nstderr:\n#{err}"
14 | end
15 |
16 | def run_tests(*args, command:, openapi: false, output: :yaml)
17 | env = {
18 | 'OPENAPI' => ('1' if openapi),
19 | 'OPENAPI_OUTPUT' => output.to_s,
20 | }.compact
21 | Bundler.public_send(Bundler.respond_to?(:with_unbundled_env) ? :with_unbundled_env : :with_clean_env) do
22 | Dir.chdir(repo_root) do
23 | assert_run env, 'bundle', 'exec', command, '-r./.simplecov_spawn', *args
24 | end
25 | end
26 | end
27 |
28 | def rspec(*args, openapi: false, output: :yaml)
29 | run_tests(*args, command: 'scripts/rspec_with_simplecov', openapi: openapi, output: output)
30 | end
31 |
32 | def minitest(*args, openapi: false, output: :yaml)
33 | run_tests(*args, command: 'ruby', openapi: openapi, output: output)
34 | end
35 | end
36 |
37 | RSpec.configure do |config|
38 | # Enable flags like --only-failures and --next-failure
39 | config.example_status_persistence_file_path = '.rspec_status'
40 |
41 | # Disable RSpec exposing methods globally on `Module` and `main`
42 | config.disable_monkey_patching!
43 |
44 | config.expect_with :rspec do |c|
45 | c.syntax = :expect
46 | end
47 | end
48 |
49 | SuperDiff.configure do |config|
50 | config.diff_elision_enabled = true
51 | end
52 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/array_hashes/mixed_types_nested.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module ArrayHashes
6 | class MixedTypesNested < HanamiTest::Action
7 | def handle(request, response)
8 | response.format = :json
9 |
10 | response.body = {
11 | "items" => [
12 | {
13 | "id" => 1,
14 | "config" => {
15 | "port" => 8080,
16 | "host" => "localhost"
17 | },
18 | "form" => [
19 | {
20 | "value" => "John Doe",
21 | "options" => [
22 | {"label" => "John Doe", "value" => "john_doe"},
23 | {"label" => "Jane Doe", "value" => "jane_doe"}
24 | ]
25 | },
26 | {
27 | "value" => [],
28 | "options" => {
29 | "endpoint" => "some/endpoint"
30 | }
31 | },
32 | {
33 | "value" => nil,
34 | "options" => nil
35 | },
36 | ]
37 | },
38 | {
39 | "id" => 2,
40 | "config" => {
41 | "port" => "3000",
42 | "host" => "example.com",
43 | "ssl" => true
44 | },
45 | "form" => nil
46 | }
47 | ]
48 | }.to_json
49 | end
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/scripts/rspec_with_simplecov:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | # (The MIT License)
5 | # Copyright (c) 2012 Chad Humphries, David Chelimsky, Myron Marston
6 | # Copyright (c) 2009 Chad Humphries, David Chelimsky
7 | # Copyright (c) 2006 David Chelimsky, The RSpec Development Team
8 | # Copyright (c) 2005 Steven Baker
9 |
10 | # Turn on verbose to make sure we not generating any ruby warning
11 | $VERBOSE = true
12 |
13 | # So our "did they run the rspec command?" detection logic thinks
14 | # that we run `rspec`.
15 | $0 = 'rspec'
16 |
17 | # This is necessary for when `--standalone` is being used.
18 | $LOAD_PATH.unshift File.expand_path '../bundle', __dir__
19 |
20 | # For the travis build we put the bundle directory up a directory
21 | # so it can be shared among the repos for faster bundle installs.
22 | $LOAD_PATH.unshift File.expand_path '../../bundle', __dir__
23 |
24 | require 'bundler/setup'
25 |
26 | # To use simplecov while running rspec-core's test suite, we must
27 | # load simplecov _before_ loading any of rspec-core's files.
28 | # So, this executable exists purely as a wrapper script that
29 | # first loads simplecov, and then loads rspec.
30 | begin
31 | # Simplecov emits some ruby warnings when loaded, so silence them.
32 | old_verbose = $VERBOSE
33 | $VERBOSE = false
34 |
35 | unless (ENV.fetch('COVERAGE', nil) && ENV['COVERAGE'].empty?) || RUBY_VERSION < '1.9.3'
36 | require 'simplecov'
37 |
38 | SimpleCov.start do
39 | enable_coverage :branch
40 | end
41 | end
42 | rescue LoadError
43 | # simplecov is not available
44 | ensure
45 | $VERBOSE = old_verbose
46 | end
47 |
48 | load File.expand_path('rspec', __dir__)
49 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::OpenAPI::Record = Struct.new(
4 | :title, # @param [String] - "API Documentation - Statuses"
5 | :http_method, # @param [String] - "GET"
6 | :path, # @param [String] - "/v1/status/:id"
7 | :path_params, # @param [Hash] - {:controller=>"v1/statuses", :action=>"create", :id=>"1"}
8 | :query_params, # @param [Hash] - {:query=>"string"}
9 | :request_params, # @param [Hash] - {:request=>"body"}
10 | :required_request_params, # @param [Array] - ["param1", "param2"]
11 | :request_content_type, # @param [String] - "application/json"
12 | :request_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
13 | :summary, # @param [String] - "v1/statuses #show"
14 | :tags, # @param [Array] - ["Status"]
15 | :formats, # @param [Proc] - ->(key) { key.end_with?('_at') ? 'date-time' : nil }
16 | :operation_id, # @param [String] - "request-1234"
17 | :description, # @param [String] - "returns a status"
18 | :security, # @param [Array] - [{securityScheme1: []}]
19 | :deprecated, # @param [Boolean] - true
20 | :status, # @param [Integer] - 200
21 | :response_body, # @param [Object] - {"status" => "ok"}
22 | :response_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]]
23 | :response_content_type, # @param [String] - "application/json"
24 | :response_content_disposition, # @param [String] - "inline"
25 | keyword_init: true,
26 | )
27 |
--------------------------------------------------------------------------------
/spec/apps/roda/doc/minitest_openapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.3",
3 | "info": {
4 | "title": "OpenAPI Documentation",
5 | "version": "7.7.7"
6 | },
7 | "servers": [],
8 | "paths": {
9 | "/roda": {
10 | "post": {
11 | "summary": "POST /roda",
12 | "requestBody": {
13 | "content": {
14 | "application/json": {
15 | "schema": {
16 | "type": "object",
17 | "properties": {
18 | "id": {
19 | "type": "integer"
20 | }
21 | },
22 | "required": [
23 | "id"
24 | ]
25 | },
26 | "example": {
27 | "id": 1
28 | }
29 | }
30 | }
31 | },
32 | "responses": {
33 | "200": {
34 | "description": "when id is given it returns 200",
35 | "content": {
36 | "application/json": {
37 | "schema": {
38 | "type": "object",
39 | "properties": {
40 | "id": {
41 | "type": "integer"
42 | },
43 | "name": {
44 | "type": "string"
45 | }
46 | },
47 | "required": [
48 | "id",
49 | "name"
50 | ]
51 | },
52 | "example": {
53 | "id": 1,
54 | "name": "hello"
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/spec/apps/roda/doc/rspec_openapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.3",
3 | "info": {
4 | "title": "OpenAPI Documentation",
5 | "version": "7.7.7"
6 | },
7 | "servers": [],
8 | "paths": {
9 | "/roda": {
10 | "post": {
11 | "summary": "Create roda resource",
12 | "requestBody": {
13 | "content": {
14 | "application/json": {
15 | "schema": {
16 | "type": "object",
17 | "properties": {
18 | "id": {
19 | "type": "integer"
20 | }
21 | },
22 | "required": [
23 | "id"
24 | ]
25 | },
26 | "example": {
27 | "id": 1
28 | }
29 | }
30 | }
31 | },
32 | "responses": {
33 | "200": {
34 | "description": "when id is given it returns 200",
35 | "content": {
36 | "application/json": {
37 | "schema": {
38 | "type": "object",
39 | "properties": {
40 | "id": {
41 | "type": "integer"
42 | },
43 | "name": {
44 | "type": "string"
45 | }
46 | },
47 | "required": [
48 | "id",
49 | "name"
50 | ]
51 | },
52 | "example": {
53 | "id": 1,
54 | "name": "hello"
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/spec/apps/rails/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
12 | #
13 | port ENV.fetch("PORT") { 3000 }
14 |
15 | # Specifies the `environment` that Puma will run in.
16 | #
17 | environment ENV.fetch("RAILS_ENV") { "development" }
18 |
19 | # Specifies the `pidfile` that Puma will use.
20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
21 |
22 | # Specifies the number of `workers` to boot in clustered mode.
23 | # Workers are forked web server processes. If using threads and workers together
24 | # the concurrency of the application would be max `threads` * `workers`.
25 | # Workers do not work on JRuby or Windows (both of which do not support
26 | # processes).
27 | #
28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
29 |
30 | # Use the `preload_app!` method when specifying a `workers` number.
31 | # This directive tells Puma to first boot the application and load code
32 | # before forking the application. This takes advantage of Copy On Write
33 | # process behavior so workers use less memory.
34 | #
35 | # preload_app!
36 |
37 | # Allow puma to be restarted by `rails restart` command.
38 | plugin :tmp_restart
39 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | mount ::MyEngine::Engine => '/my_engine'
3 | mount ::RackTest::App.new, at: '/rack'
4 |
5 | get '/my_engine/test' => ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['ANOTHER TEST']] }
6 |
7 | defaults format: :html do
8 | get '/pages' => 'pages#get'
9 | end
10 |
11 | defaults format: 'json' do
12 | resources :sites, param: :name, only: [:show]
13 | resources :tables, only: [:index, :show, :create, :update, :destroy]
14 | resources :images, only: [:index, :show] do
15 | collection do
16 | post 'upload'
17 | post 'upload_nested'
18 | post 'upload_multiple'
19 | post 'upload_multiple_nested'
20 | end
21 | end
22 | resources :users, only: [:show, :create] do
23 | get 'active'
24 | end
25 |
26 | get '/test_block' => ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['A TEST']] }
27 |
28 | get '/secret_items' => 'secret_items#index'
29 |
30 | get '/additional_properties' => 'additional_properties#index'
31 | get '/invalid_responses' => 'invalid_responses#show'
32 | resources :array_hashes, only: [] do
33 | get :nullable, on: :collection
34 | get :non_nullable, on: :collection
35 | get :nested, on: :collection
36 | get :empty_array, on: :collection
37 | get :single_item, on: :collection
38 | get :non_hash_items, on: :collection
39 | get :nested_arrays, on: :collection
40 | get :nested_objects, on: :collection
41 | get :mixed_types_nested, on: :collection
42 | end
43 |
44 | scope :admin do
45 | namespace :masters do
46 | resources :extensions, only: [:index, :create]
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # The test environment is used exclusively to run your application's
2 | # test suite. You never need to work with it otherwise. Remember that
3 | # your test database is "scratch space" for the test suite and is wiped
4 | # and recreated between test runs. Don't rely on the data there!
5 | require 'active_support/time'
6 |
7 | Rails.application.configure do
8 | # Settings specified here will take precedence over those in config/application.rb.
9 |
10 | config.cache_classes = false
11 | config.action_view.cache_template_loading = true
12 |
13 | # Do not eager load code on boot. This avoids loading your whole application
14 | # just for the purpose of running a single test. If you are using a tool that
15 | # preloads Rails for running tests, you may have to set it to true.
16 | config.eager_load = false
17 |
18 | # Configure public file server for tests with Cache-Control for performance.
19 | config.public_file_server.enabled = true
20 | config.public_file_server.headers = {
21 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}",
22 | }
23 |
24 | # Show full error reports and disable caching.
25 | config.consider_all_requests_local = true
26 | config.action_controller.perform_caching = false
27 | config.cache_store = :null_store
28 |
29 | # Raise exceptions instead of rendering exception templates.
30 | config.action_dispatch.show_exceptions = false
31 |
32 | # Disable request forgery protection in test environment.
33 | config.action_controller.allow_forgery_protection = false
34 |
35 | # Print deprecation notices to the stderr.
36 | config.active_support.deprecation = :stderr
37 |
38 | # Raises error for missing translations.
39 | # config.action_view.raise_on_missing_translations = true
40 | end
41 |
--------------------------------------------------------------------------------
/.rubocop_todo.yml:
--------------------------------------------------------------------------------
1 | # This configuration was generated by
2 | # `rubocop --auto-gen-config`
3 | # on 2024-04-20 21:25:22 UTC using RuboCop version 1.62.1.
4 | # The point is for the user to remove these configuration records
5 | # one by one as the offenses are removed from the code base.
6 | # Note that changes in the inspected code, or installation of new
7 | # versions of RuboCop, may require this file to be generated again.
8 |
9 | # Offense count: 14
10 | # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
11 | Metrics/AbcSize:
12 | Max: 46
13 |
14 | # Offense count: 1
15 | # Configuration parameters: CountComments, CountAsOne.
16 | Metrics/ClassLength:
17 | Max: 195
18 |
19 | # Offense count: 9
20 | # Configuration parameters: AllowedMethods, AllowedPatterns.
21 | Metrics/CyclomaticComplexity:
22 | Max: 13
23 |
24 | # Offense count: 22
25 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
26 | Metrics/MethodLength:
27 | Max: 36
28 |
29 | # Offense count: 5
30 | # Configuration parameters: AllowedMethods, AllowedPatterns.
31 | Metrics/PerceivedComplexity:
32 | Max: 13
33 |
34 | # Offense count: 1
35 | # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
36 | # SupportedStyles: snake_case, normalcase, non_integer
37 | # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
38 | Naming/VariableNumber:
39 | Exclude:
40 | - 'spec/integration_tests/roda_test.rb'
41 |
42 | # Offense count: 7
43 | # Configuration parameters: AllowedConstants.
44 | Style/Documentation:
45 | Exclude:
46 | - 'spec/**/*'
47 | - 'test/**/*'
48 | - 'lib/rspec/openapi.rb'
49 | - 'lib/rspec/openapi/minitest_hooks.rb'
50 | - 'lib/rspec/openapi/result_recorder.rb'
51 | - 'lib/rspec/openapi/schema_file.rb'
52 | - 'lib/rspec/openapi/shared_hooks.rb'
53 |
--------------------------------------------------------------------------------
/spec/apps/rails/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/spec/apps/rails/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/spec/apps/rails/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/spec/rspec/rails_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 | require 'yaml'
5 | require 'json'
6 | require 'pry'
7 |
8 | RSpec.describe 'rails request spec' do
9 | include SpecHelper
10 |
11 | describe 'yaml output' do
12 | let(:openapi_path) do
13 | File.expand_path('spec/apps/rails/doc/rspec_openapi.yaml', repo_root)
14 | end
15 |
16 | it 'generates the same spec/apps/rails/doc/rspec_openapi.yaml' do
17 | org_yaml = YAML.safe_load(File.read(openapi_path))
18 | rspec 'spec/requests/rails_spec.rb', openapi: true, output: :yaml
19 | new_yaml = YAML.safe_load(File.read(openapi_path))
20 | expect(new_yaml).to eq org_yaml
21 | end
22 | end
23 |
24 | describe 'json' do
25 | let(:openapi_path) do
26 | File.expand_path('spec/apps/rails/doc/rspec_openapi.json', repo_root)
27 | end
28 |
29 | it 'generates the same spec/apps/rails/doc/rspec_openapi.json' do
30 | org_json = JSON.parse(File.read(openapi_path))
31 | rspec 'spec/requests/rails_spec.rb', openapi: true, output: :json
32 | new_json = JSON.parse(File.read(openapi_path))
33 | expect(new_json).to eq org_json
34 | end
35 | end
36 |
37 | describe 'smart merge' do
38 | let(:openapi_path) do
39 | File.expand_path('spec/apps/rails/doc/smart/openapi.yaml', repo_root)
40 | end
41 |
42 | let(:expected_path) do
43 | File.expand_path('spec/apps/rails/doc/smart/expected.yaml', repo_root)
44 | end
45 |
46 | it 'updates the spec/apps/rails/doc/smart/openapi.yaml as same as in expected.yaml' do
47 | original_source = File.read(openapi_path)
48 | begin
49 | rspec 'spec/requests/rails_smart_merge_spec.rb', openapi: true, output: :yaml
50 | new_yaml = YAML.safe_load(File.read(openapi_path))
51 | expected_yaml = YAML.safe_load(File.read(expected_path))
52 | expect(new_yaml).to eq expected_yaml
53 | ensure
54 | File.write(openapi_path, original_source)
55 | end
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/spec/apps/hanami/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | We’re sorry, but something went wrong (500)
7 |
72 |
73 |
74 |
75 |
76 |
77 |
500
78 |
We’re sorry, but something went wrong.
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/spec/apps/hanami/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | The page you were looking for doesn’t exist (404)
7 |
72 |
73 |
74 |
75 |
76 |
77 |
404
78 |
The page you were looking for doesn’t exist.
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/extractors/rack.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Extractor for rack
4 | class << RSpec::OpenAPI::Extractors::Rack = Object.new
5 | # @param [ActionDispatch::Request] request
6 | # @param [RSpec::Core::Example] example
7 | # @return Array
8 | def request_attributes(request, example)
9 | metadata = merge_openapi_metadata(example.metadata)
10 | summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
11 | tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
12 | formats = metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example)
13 | operation_id = metadata[:operation_id]
14 | required_request_params = metadata[:required_request_params] || []
15 | security = metadata[:security]
16 | description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example)
17 | deprecated = metadata[:deprecated]
18 | raw_path_params = request.path_parameters
19 | path = request.path
20 | summary ||= "#{request.method} #{path}"
21 | [
22 | path,
23 | summary,
24 | tags,
25 | operation_id,
26 | required_request_params,
27 | raw_path_params,
28 | description,
29 | security,
30 | deprecated,
31 | formats,
32 | ]
33 | end
34 |
35 | # @param [RSpec::ExampleGroups::*] context
36 | def request_response(context)
37 | request = ActionDispatch::Request.new(context.last_request.env)
38 | request.body.rewind if request.body.respond_to?(:rewind)
39 | response = ActionDispatch::TestResponse.new(*context.last_response.to_a)
40 |
41 | [request, response]
42 | end
43 |
44 | private
45 |
46 | def merge_openapi_metadata(metadata)
47 | collect_openapi_metadata(metadata).reduce({}, &:merge)
48 | end
49 |
50 | def collect_openapi_metadata(metadata)
51 | [].tap do |result|
52 | current = metadata
53 |
54 | while current
55 | [current[:example_group], current].each do |meta|
56 | result.unshift(meta[:openapi]) if meta&.dig(:openapi)
57 | end
58 |
59 | current = current[:parent_example_group]
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/tables_controller.rb:
--------------------------------------------------------------------------------
1 | class TablesController < ApplicationController
2 | APIKEY = 'k0kubun'.freeze
3 |
4 | before_action :authenticate
5 |
6 | def index
7 | response.set_header('X-Cursor', 100)
8 | if params[:show_columns]
9 | render json: [find_table('42')]
10 | else
11 | render json: [find_table]
12 | end
13 | end
14 |
15 | def show
16 | render json: find_table(params[:id])
17 | end
18 |
19 | def create
20 | if params[:name].blank? || params[:name] == 'some_invalid_name'
21 | render json: { error: 'invalid name parameter' }, status: 422
22 | else
23 | render json: find_table, status: 201
24 | end
25 | end
26 |
27 | def update
28 | render json: find_table(params[:id])
29 | end
30 |
31 | def destroy
32 | if params[:no_content]
33 | return head 202
34 | end
35 |
36 | render json: find_table(params[:id])
37 | end
38 |
39 | private
40 |
41 | def authenticate
42 | if request.headers[:authorization] != APIKEY
43 | render json: { message: 'Unauthorized' }, status: 401
44 | end
45 | end
46 |
47 | def find_table(id = nil)
48 | time = Time.parse('2020-07-17 00:00:00')
49 | case id
50 | when '1', nil
51 | {
52 | id: 1,
53 | name: 'access',
54 | description: 'logs',
55 | database: {
56 | id: 2,
57 | name: 'production',
58 | },
59 | null_sample: nil,
60 | storage_size: 12.3,
61 | created_at: time.iso8601,
62 | updated_at: time.iso8601,
63 | }
64 | when '42'
65 | {
66 | id: 42,
67 | name: 'access',
68 | description: 'logs',
69 | database: {
70 | id: 4242,
71 | name: 'production',
72 | },
73 | columns: [
74 | { name: 'id', column_type: 'integer' },
75 | { name: 'description', column_type: 'varchar' },
76 | ],
77 | null_sample: nil,
78 | storage_size: 12.3,
79 | created_at: time.iso8601,
80 | updated_at: time.iso8601,
81 | }
82 | else
83 | raise NotFoundError
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/spec/apps/hanami/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | require 'rack_test/app'
3 |
4 | module HanamiTest
5 | class Routes < Hanami::Routes
6 | # Add your routes here. See https://guides.hanamirb.org/routing/overview/ for details.
7 | get '/secret_items', to: 'secret_items.index'
8 |
9 | get '/tables', to: 'tables.index'
10 | get '/tables/:id', to: 'tables.show'
11 | post '/tables', to: 'tables.create'
12 | patch '/tables/:id', to: 'tables.update'
13 | delete '/tables/:id', to: 'tables.destroy'
14 |
15 | get '/images', to: 'images.index'
16 | get '/images/:id', to: 'images.show'
17 | post '/images/upload', to: 'images.upload'
18 | post '/images/upload_nested', to: 'images.upload_nested'
19 | post '/images/upload_multiple', to: 'images.upload_multiple'
20 | post '/images/upload_multiple_nested', to: 'images.upload_multiple_nested'
21 |
22 | post '/users', to: 'users.create'
23 | get '/users/:id', to: 'users.show'
24 | get '/users/active', to: 'users.active'
25 |
26 | get '/sites/:name', to: 'sites.show'
27 | get '/array_hashes/nullable', to: 'array_hashes.nullable'
28 | get '/array_hashes/non_nullable', to: 'array_hashes.non_nullable'
29 | get '/array_hashes/nested', to: 'array_hashes.nested'
30 | get '/array_hashes/empty_array', to: 'array_hashes.empty_array'
31 | get '/array_hashes/single_item', to: 'array_hashes.single_item'
32 | get '/array_hashes/non_hash_items', to: 'array_hashes.non_hash_items'
33 | get '/array_hashes/nested_arrays', to: 'array_hashes.nested_arrays'
34 | get '/array_hashes/nested_objects', to: 'array_hashes.nested_objects'
35 | get '/array_hashes/mixed_types_nested', to: 'array_hashes.mixed_types_nested'
36 |
37 | get '/test_block', to: ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['A TEST']] }
38 |
39 | slice :my_engine, at: '/my_engine' do
40 | get '/test', to: ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['ANOTHER TEST']] }
41 | get '/eng/example', to: 'eng.example'
42 | end
43 |
44 | scope 'admin' do
45 | scope 'masters' do
46 | get '/extensions', to: 'extensions.index'
47 | post '/extensions', to: 'extensions.create'
48 | end
49 | end
50 |
51 | use RackTest::App
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/result_recorder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class RSpec::OpenAPI::ResultRecorder
4 | def initialize(path_records)
5 | @path_records = path_records
6 | @error_records = {}
7 | end
8 |
9 | def record_results!
10 | @path_records.each do |path, records|
11 | # Look for a path-specific config file and run it.
12 | config_file = File.join(File.dirname(path), RSpec::OpenAPI.config_filename)
13 | begin
14 | require config_file if File.exist?(config_file)
15 | rescue StandardError => e
16 | puts "WARNING: Unable to load #{config_file}: #{e}"
17 | end
18 |
19 | title = records.first.title
20 | RSpec::OpenAPI::SchemaFile.new(path).edit do |spec|
21 | schema = RSpec::OpenAPI::DefaultSchema.build(title)
22 | schema[:info].merge!(RSpec::OpenAPI.info)
23 | RSpec::OpenAPI::SchemaMerger.merge!(spec, schema)
24 | new_from_zero = {}
25 | records.each do |record|
26 | record_schema = RSpec::OpenAPI::SchemaBuilder.build(record)
27 | RSpec::OpenAPI::SchemaMerger.merge!(spec, record_schema)
28 | RSpec::OpenAPI::SchemaMerger.merge!(new_from_zero, record_schema)
29 | rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
30 | @error_records[e] = record # Avoid failing the build
31 | end
32 | cleanup_schema!(new_from_zero, spec)
33 | execute_post_process_hook(path, records, spec)
34 | end
35 | end
36 | end
37 |
38 | def errors?
39 | @error_records.any?
40 | end
41 |
42 | def error_message
43 | <<~ERR_MSG
44 | RSpec::OpenAPI got errors building #{@error_records.size} requests
45 |
46 | #{@error_records.map { |e, record| "#{e.inspect}: #{record.inspect}" }.join("\n")}
47 | ERR_MSG
48 | end
49 |
50 | private
51 |
52 | def execute_post_process_hook(path, records, spec)
53 | RSpec::OpenAPI.post_process_hook.call(path, records, spec) if RSpec::OpenAPI.post_process_hook.is_a?(Proc)
54 | end
55 |
56 | def cleanup_schema!(new_from_zero, spec)
57 | RSpec::OpenAPI::SchemaCleaner.cleanup_conflicting_security_parameters!(spec)
58 | RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
59 | RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
60 | RSpec::OpenAPI::SchemaCleaner.cleanup_empty_required_array!(spec)
61 | RSpec::OpenAPI::SchemaSorter.deep_sort!(spec)
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | # Run rails dev:cache to toggle caching.
17 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
18 | config.action_controller.perform_caching = true
19 | config.action_controller.enable_fragment_cache_logging = true
20 |
21 | config.cache_store = :memory_store
22 | config.public_file_server.headers = {
23 | 'Cache-Control' => "public, max-age=#{2.days.to_i}",
24 | }
25 | else
26 | config.action_controller.perform_caching = false
27 |
28 | config.cache_store = :null_store
29 | end
30 |
31 | # Store uploaded files on the local file system (see config/storage.yml for options).
32 | config.active_storage.service = :local
33 |
34 | # Don't care if the mailer can't send.
35 | config.action_mailer.raise_delivery_errors = false
36 |
37 | config.action_mailer.perform_caching = false
38 |
39 | # Print deprecation notices to the Rails logger.
40 | config.active_support.deprecation = :log
41 |
42 | # Raise an error on page load if there are pending migrations.
43 | config.active_record.migration_error = :page_load
44 |
45 | # Highlight code that triggered database queries in logs.
46 | config.active_record.verbose_query_logs = true
47 |
48 | # Debug mode disables concatenation and preprocessing of assets.
49 | # This option may cause significant delays in view rendering with a large
50 | # number of complex assets.
51 | config.assets.debug = true
52 |
53 | # Suppress logger output for asset requests.
54 | config.assets.quiet = true
55 |
56 | # Raises error for missing translations.
57 | # config.action_view.raise_on_missing_translations = true
58 |
59 | # Use an evented file watcher to asynchronously detect changes in source code,
60 | # routes, locales, etc. This feature depends on the listen gem.
61 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
62 | end
63 |
--------------------------------------------------------------------------------
/spec/apps/hanami/app/actions/array_hashes/nested_objects.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module HanamiTest
4 | module Actions
5 | module ArrayHashes
6 | class NestedObjects < HanamiTest::Action
7 | def handle(request, response)
8 | response.format = :json
9 |
10 | response.body = {
11 | "items" => [
12 | {
13 | "id" => 1,
14 | "metadata" => {
15 | "author" => "Alice",
16 | "version" => "1.0"
17 | },
18 | "actions" => [
19 | {
20 | "label" => "Duplicate",
21 | "modal" => {
22 | "confirm_action" => {
23 | "label" => "Duplicate",
24 | "endpoint" => nil
25 | }
26 | }
27 | },
28 | {
29 | "label" => "Edit",
30 | },
31 | {
32 | "label" => "Something Else Again",
33 | "modal" => {
34 | "confirm_action" => {
35 | "label" => nil,
36 | "endpoint" => nil
37 | }
38 | }
39 | }
40 | ]
41 | },
42 | {
43 | "id" => 2,
44 | "metadata" => {
45 | "author" => "Bob",
46 | "version" => "2.0",
47 | "reviewed" => true
48 | },
49 | "actions" => [
50 | {
51 | "label" => "Duplicate",
52 | "modal" => {
53 | "confirm_action" => {
54 | "label" => "Duplicate",
55 | "endpoint" => nil
56 | }
57 | }
58 | },
59 | {
60 | "label" => "Edit",
61 | },
62 | {
63 | "label" => "Something Else Again",
64 | "modal" => {
65 | "confirm_action" => {
66 | "label" => nil,
67 | "endpoint" => nil
68 | }
69 | }
70 | }
71 | ]
72 | },
73 | {
74 | "id" => 3,
75 | "metadata" => {
76 | "author" => "Charlie"
77 | }
78 | }
79 | ]
80 | }.to_json
81 | end
82 | end
83 | end
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/.github/workflows/create_release.yml:
--------------------------------------------------------------------------------
1 | name: prepare release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: 'Version to release (e.g. 0.21.3 or v0.21.3)'
8 | required: true
9 |
10 | jobs:
11 | push:
12 | name: Prepare release PR
13 | runs-on: ubuntu-latest
14 |
15 | permissions:
16 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
17 | contents: write # required to push release branch and open PR
18 | pull-requests: write # required to create the release PR with GITHUB_TOKEN
19 |
20 | steps:
21 | - uses: actions/checkout@v5
22 | with:
23 | fetch-depth: 0
24 |
25 | - name: Bump version.rb
26 | run: |
27 | set -euo pipefail
28 | version="${{ github.event.inputs.version }}"
29 | version="${version#v}"
30 |
31 | echo "VERSION_NO_V=$version" >> "$GITHUB_ENV"
32 | echo "VERSION_TAG=v$version" >> "$GITHUB_ENV"
33 |
34 | current_version=$(ruby -e "require_relative './lib/rspec/openapi/version'; puts RSpec::OpenAPI::VERSION")
35 | VERSION_NO_V="$version" CURRENT_VERSION="$current_version" ruby - <<'RUBY'
36 | version = ENV.fetch('VERSION_NO_V')
37 | unless version.match?(/\A\d+(?:\.\d+)*\z/)
38 | warn "Invalid version format: #{version}"
39 | exit 1
40 | end
41 |
42 | require 'rubygems'
43 | new_version = Gem::Version.new(version)
44 | current_version = Gem::Version.new(ENV.fetch('CURRENT_VERSION'))
45 | if new_version <= current_version
46 | warn "Given version (#{new_version}) must be newer than current version (#{current_version})"
47 | exit 1
48 | end
49 | RUBY
50 |
51 | ruby -pi -e "sub(/VERSION = .*/, \"VERSION = '$version'\")" lib/rspec/openapi/version.rb
52 | git status --short
53 |
54 | - name: Commit version bump
55 | run: |
56 | set -euo pipefail
57 | version="$VERSION_NO_V"
58 |
59 | release_branch="release/v${version}"
60 | echo "RELEASE_BRANCH=${release_branch}" >> "$GITHUB_ENV"
61 |
62 | git config user.name "github-actions[bot]"
63 | git config user.email "github-actions[bot]@users.noreply.github.com"
64 | git commit -am "Bump version to ${version}"
65 | git push origin "HEAD:${release_branch}"
66 |
67 | - name: Open release PR
68 | uses: peter-evans/create-pull-request@v6
69 | with:
70 | token: ${{ secrets.GITHUB_TOKEN }}
71 | add-paths: |
72 | lib/rspec/openapi/version.rb
73 | branch: ${{ env.RELEASE_BRANCH }}
74 | title: Release v${{ env.VERSION_NO_V }}
75 | commit-message: Bump version to ${{ env.VERSION_NO_V }}
76 | body: |
77 | Automated release PR created by workflow_dispatch.
78 | - Version: v${{ env.VERSION_NO_V }}
79 | - Triggered by: ${{ github.actor }}
80 |
--------------------------------------------------------------------------------
/lib/rspec/openapi.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rspec/openapi/version'
4 | require 'rspec/openapi/components_updater'
5 | require 'rspec/openapi/default_schema'
6 | require 'rspec/openapi/record_builder'
7 | require 'rspec/openapi/result_recorder'
8 | require 'rspec/openapi/schema_builder'
9 | require 'rspec/openapi/schema_file'
10 | require 'rspec/openapi/schema_merger'
11 | require 'rspec/openapi/schema_cleaner'
12 | require 'rspec/openapi/schema_sorter'
13 | require 'rspec/openapi/key_transformer'
14 | require 'rspec/openapi/shared_hooks'
15 | require 'rspec/openapi/extractors'
16 | require 'rspec/openapi/extractors/rack'
17 |
18 | module RSpec::OpenAPI
19 | class Config
20 | class << self
21 | attr_accessor :debug_enabled
22 |
23 | def load_environment_settings
24 | @debug_enabled = ['', '1', 'true'].include?(ENV['DEBUG']&.downcase)
25 | end
26 | end
27 | end
28 |
29 | @path = 'doc/openapi.yaml'
30 | @title = File.basename(Dir.pwd)
31 | @comment = nil
32 | @enable_example = true
33 | @description_builder = ->(example) { example.description }
34 | @summary_builder = ->(example) { example.metadata[:summary] }
35 | @tags_builder = ->(example) { example.metadata[:tags] }
36 | @formats_builder = ->(example) { example.metadata[:formats] }
37 | @info = {}
38 | @application_version = '1.0.0'
39 | @request_headers = []
40 | @servers = []
41 | @security_schemes = []
42 | @example_types = %i[request]
43 | @response_headers = []
44 | @path_records = Hash.new { |h, k| h[k] = [] }
45 | @ignored_path_params = %i[controller action format]
46 | @ignored_paths = []
47 | @post_process_hook = nil
48 |
49 | # This is the configuraion override file name we look for within each path.
50 | @config_filename = 'rspec_openapi.rb'
51 |
52 | class << self
53 | attr_accessor :path,
54 | :title,
55 | :comment,
56 | :enable_example,
57 | :description_builder,
58 | :summary_builder,
59 | :tags_builder,
60 | :formats_builder,
61 | :info,
62 | :application_version,
63 | :request_headers,
64 | :servers,
65 | :security_schemes,
66 | :example_types,
67 | :response_headers,
68 | :path_records,
69 | :ignored_paths,
70 | :ignored_path_params,
71 | :post_process_hook
72 |
73 | attr_reader :config_filename
74 | end
75 | end
76 |
77 | if ENV['OPENAPI']
78 | RSpec::OpenAPI::Config.load_environment_settings
79 |
80 | begin
81 | require 'hanami'
82 | rescue LoadError
83 | warn 'Hanami not detected' if RSpec::OpenAPI::Config.debug_enabled
84 | else
85 | require 'rspec/openapi/extractors/hanami'
86 | end
87 |
88 | begin
89 | require 'rails'
90 | rescue LoadError
91 | warn 'Rails not detected' if RSpec::OpenAPI::Config.debug_enabled
92 | else
93 | require 'rspec/openapi/extractors/rails'
94 | end
95 | end
96 |
97 | require 'rspec/openapi/minitest_hooks' if Object.const_defined?('Minitest')
98 | require 'rspec/openapi/rspec_hooks' if ENV['OPENAPI'] && Object.const_defined?('RSpec')
99 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/record_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'action_dispatch'
4 | require 'rspec/openapi/record'
5 |
6 | class << RSpec::OpenAPI::RecordBuilder = Object.new
7 | # @param [RSpec::ExampleGroups::*] context
8 | # @param [RSpec::Core::Example] example
9 | # @return [RSpec::OpenAPI::Record,nil]
10 | def build(context, example:, extractor:)
11 | request, response = extractor.request_response(context)
12 | return if request.nil?
13 |
14 | title = RSpec::OpenAPI.title.then { |t| t.is_a?(Proc) ? t.call(example) : t }
15 | path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated, formats =
16 | extractor.request_attributes(request, example)
17 |
18 | return if RSpec::OpenAPI.ignored_paths.any? { |ignored_path| path.match?(ignored_path) }
19 |
20 | request_headers, response_headers = extract_headers(request, response)
21 |
22 | RSpec::OpenAPI::Record.new(
23 | title: title,
24 | http_method: request.method,
25 | path: path,
26 | path_params: raw_path_params,
27 | query_params: request.query_parameters,
28 | request_params: raw_request_params(request),
29 | required_request_params: required_request_params,
30 | request_content_type: request.media_type,
31 | request_headers: request_headers,
32 | summary: summary,
33 | tags: tags,
34 | formats: formats,
35 | operation_id: operation_id,
36 | description: description,
37 | security: security,
38 | deprecated: deprecated,
39 | status: response.status,
40 | response_body: safe_parse_body(response, response.media_type),
41 | response_headers: response_headers,
42 | response_content_type: response.media_type,
43 | response_content_disposition: response.header['Content-Disposition'],
44 | ).freeze
45 | end
46 |
47 | private
48 |
49 | def safe_parse_body(response, media_type)
50 | # Use raw body, because Nokogiri-parsed HTML are modified (new lines injection, meta injection, and so on) :(
51 | return response.body if media_type == 'text/html'
52 |
53 | response.parsed_body
54 | rescue JSON::ParserError
55 | nil
56 | end
57 |
58 | def extract_headers(request, response)
59 | request_headers = RSpec::OpenAPI.request_headers.each_with_object([]) do |header, headers_arr|
60 | header_key = header.gsub('-', '_').upcase.to_sym
61 |
62 | header_value = request.get_header(['HTTP', header_key].join('_')) ||
63 | request.get_header(header_key) ||
64 | request.get_header(header_key.to_s)
65 | headers_arr << [header, header_value] if header_value
66 | end
67 | response_headers = RSpec::OpenAPI.response_headers.each_with_object([]) do |header, headers_arr|
68 | header_key = header
69 | header_value = response.headers[header_key]
70 | headers_arr << [header_key, header_value] if header_value
71 | end
72 | [request_headers, response_headers]
73 | end
74 |
75 | # workaround to get real request parameters
76 | # because ActionController::ParamsWrapper overwrites request_parameters
77 | def raw_request_params(request)
78 | original = request.delete_header('action_dispatch.request.request_parameters')
79 | request.request_parameters
80 | ensure
81 | request.set_header('action_dispatch.request.request_parameters', original)
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/extractors/rails.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Extractor for rails
4 | class << RSpec::OpenAPI::Extractors::Rails = Object.new
5 | # @param [ActionDispatch::Request] request
6 | # @param [RSpec::Core::Example] example
7 | # @return Array
8 | def request_attributes(request, example)
9 | # Reverse the destructive modification by Rails https://github.com/rails/rails/blob/v6.0.3.4/actionpack/lib/action_dispatch/journey/router.rb#L33-L41
10 | fixed_request = request.dup
11 | fixed_request.path_info = File.join(request.script_name, request.path_info) if request.script_name.present?
12 |
13 | route, path = find_rails_route(fixed_request)
14 |
15 | return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless path
16 |
17 | raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil?
18 |
19 | metadata = merge_openapi_metadata(example.metadata)
20 | summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
21 | tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
22 | formats = metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example)
23 |
24 | operation_id = metadata[:operation_id]
25 | required_request_params = metadata[:required_request_params] || []
26 | security = metadata[:security]
27 | description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example)
28 | deprecated = metadata[:deprecated]
29 | raw_path_params = request.path_parameters
30 |
31 | summary ||= route.requirements[:action]
32 | tags ||= [route.requirements[:controller]&.classify].compact
33 | # :controller and :action always exist. :format is added when routes is configured as such.
34 | # TODO: Use .except(:controller, :action, :format) when we drop support for Ruby 2.x
35 | raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params))
36 |
37 | summary ||= "#{request.method} #{path}"
38 |
39 | [
40 | path,
41 | summary,
42 | tags,
43 | operation_id,
44 | required_request_params,
45 | raw_path_params,
46 | description,
47 | security,
48 | deprecated,
49 | formats,
50 | ]
51 | end
52 |
53 | # @param [RSpec::ExampleGroups::*] context
54 | def request_response(context)
55 | [context.request, context.response]
56 | end
57 |
58 | private
59 |
60 | def merge_openapi_metadata(metadata)
61 | collect_openapi_metadata(metadata).reduce({}, &:merge)
62 | end
63 |
64 | def collect_openapi_metadata(metadata)
65 | [].tap do |result|
66 | current = metadata
67 |
68 | while current
69 | [current[:example_group], current].each do |meta|
70 | result.unshift(meta[:openapi]) if meta&.dig(:openapi)
71 | end
72 |
73 | current = current[:parent_example_group]
74 | end
75 | end
76 | end
77 |
78 | # @param [ActionDispatch::Request] request
79 | def find_rails_route(request, app: Rails.application, path_prefix: '')
80 | app.routes.router.recognize(request) do |route, _parameters|
81 | path = route.path.spec.to_s.delete_suffix('(.:format)')
82 |
83 | if route.app.matches?(request)
84 | if route.app.engine?
85 | route, path = find_rails_route(request, app: route.app.app, path_prefix: path)
86 | next if route.nil?
87 | end
88 |
89 | # Params are empty when it is Engine or Rack app.
90 | # In that case, we can't handle parameters in path.
91 | return [route, nil] if request.params.empty?
92 |
93 | return [route, path_prefix + path]
94 | end
95 | end
96 |
97 | nil
98 | end
99 | end
100 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/schema_merger.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class << RSpec::OpenAPI::SchemaMerger = Object.new
4 | # @param [Hash] base
5 | # @param [Hash] spec
6 | def merge!(base, spec)
7 | spec = RSpec::OpenAPI::KeyTransformer.symbolize(spec)
8 | base.replace(RSpec::OpenAPI::KeyTransformer.symbolize(base))
9 | merge_schema!(base, spec)
10 | end
11 |
12 | private
13 |
14 | # Not doing `base.replace(deep_merge(base, spec))` to preserve key orders.
15 | # Also this needs to be aware of OpenAPI details because a Hash-like structure
16 | # may be an array whose Hash elements have a key name.
17 | #
18 | # TODO: Should we probably force-merge `summary` regardless of manual modifications?
19 | def merge_schema!(base, spec)
20 | if (options = base[:oneOf])
21 | merge_closest_match!(options, spec)
22 |
23 | return base
24 | end
25 |
26 | spec.each do |key, value|
27 | if base[key].is_a?(Hash) && value.is_a?(Hash)
28 | # If the new value has oneOf, replace the entire value instead of merging
29 | if value.key?(:oneOf)
30 | base[key] = value
31 | else
32 | merge_schema!(base[key], value) unless base[key].key?(:$ref)
33 | end
34 | elsif base[key].is_a?(Array) && value.is_a?(Array)
35 | # parameters need to be merged as if `name` and `in` were the Hash keys.
36 | merge_arrays(base, key, value)
37 | else
38 | # do not ADD `properties` or `required` fields if `additionalProperties` field is present
39 | base[key] = value unless base.key?(:additionalProperties) && %i[properties required].include?(key)
40 | end
41 | end
42 | base
43 | end
44 |
45 | def merge_arrays(base, key, value)
46 | base[key] = case key
47 | when :parameters
48 | merge_parameters(base, key, value)
49 | when :required
50 | # Preserve properties that appears in all test cases
51 | value & base[key]
52 | else
53 | # last one wins
54 | value
55 | end
56 | end
57 |
58 | def merge_parameters(base, key, value)
59 | all_parameters = value | base[key]
60 |
61 | unique_base_parameters = build_unique_params(base, key)
62 |
63 | all_parameters = all_parameters.map do |parameter|
64 | base_parameter = unique_base_parameters[[parameter[:name], parameter[:in]]] || {}
65 | base_parameter ? base_parameter.merge(parameter) : parameter
66 | end
67 |
68 | all_parameters.uniq! { |param| param.slice(:name, :in) }
69 | base[key] = all_parameters
70 | end
71 |
72 | def build_unique_params(base, key)
73 | base[key].each_with_object({}) do |parameter, hash|
74 | hash[[parameter[:name], parameter[:in]]] = parameter
75 | end
76 | end
77 |
78 | SIMILARITY_THRESHOLD = 0.5
79 |
80 | def merge_closest_match!(options, spec)
81 | score, option = options.map { |option| [similarity(option, spec), option] }.max_by(&:first)
82 |
83 | return if option&.key?(:$ref)
84 |
85 | return if spec[:oneOf]
86 |
87 | if score.to_f > SIMILARITY_THRESHOLD
88 | merge_schema!(option, spec)
89 | else
90 | options.push(spec)
91 | end
92 | end
93 |
94 | def similarity(first, second)
95 | return 1 if first == second
96 |
97 | score =
98 | case [first.class, second.class]
99 | when [Array, Array]
100 | (first & second).size / [first.size, second.size].max.to_f
101 | when [Hash, Hash]
102 | return 1 if first.merge(second).key?(:$ref)
103 |
104 | intersection = first.keys & second.keys
105 | total_size = [first.size, second.size].max.to_f
106 |
107 | intersection.sum { |key| similarity(first[key], second[key]) } / total_size
108 | else
109 | 0
110 | end
111 |
112 | score.finite? ? score : 0
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/extractors/hanami.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'dry/inflector'
4 | require 'hanami'
5 |
6 | # https://github.com/hanami/router/blob/97f75b8529574bd4ff23165460e82a6587bc323c/lib/hanami/router/inspector.rb#L13
7 | class Inspector
8 | attr_accessor :routes, :inflector
9 |
10 | def initialize(routes: [])
11 | @routes = routes
12 | @inflector = Dry::Inflector.new
13 | end
14 |
15 | def add_route(route)
16 | routes.push(route)
17 | end
18 |
19 | def call(verb, path)
20 | route = routes.find { |r| r.http_method == verb && r.path == path }
21 |
22 | if route.to.is_a?(Proc)
23 | {
24 | tags: [],
25 | summary: "#{verb} #{path}",
26 | }
27 | else
28 | data = route.to.split('.')
29 |
30 | {
31 | tags: [inflector.classify(data[0])],
32 | summary: data[1],
33 | }
34 | end
35 | end
36 | end
37 |
38 | InspectorAnalyzer = Inspector.new
39 |
40 | # Add default parameter to load inspector before test cases run
41 | module InspectorAnalyzerPrepender
42 | def router(inspector: InspectorAnalyzer)
43 | super
44 | end
45 | end
46 |
47 | Hanami::Slice::ClassMethods.prepend(InspectorAnalyzerPrepender)
48 |
49 | # Extractor for hanami
50 | class << RSpec::OpenAPI::Extractors::Hanami = Object.new
51 | # @param [ActionDispatch::Request] request
52 | # @param [RSpec::Core::Example] example
53 | # @return Array
54 | def request_attributes(request, example)
55 | route = Hanami.app.router.recognize(Rack::MockRequest.env_for(request.path, method: request.method))
56 |
57 | return RSpec::OpenAPI::Extractors::Rack.request_attributes(request, example) unless route.routable?
58 |
59 | metadata = merge_openapi_metadata(example.metadata)
60 | summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example)
61 | tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example)
62 | formats = metadata[:formats] || RSpec::OpenAPI.formats_builder.curry.call(example)
63 | operation_id = metadata[:operation_id]
64 | required_request_params = metadata[:required_request_params] || []
65 | security = metadata[:security]
66 | description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example)
67 | deprecated = metadata[:deprecated]
68 | path = request.path
69 |
70 | raw_path_params = route.params
71 |
72 | result = InspectorAnalyzer.call(request.method, add_id(path, route))
73 |
74 | summary ||= result[:summary]
75 | tags ||= result[:tags]
76 | path = add_openapi_id(path, route)
77 |
78 | raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params))
79 |
80 | [
81 | path,
82 | summary,
83 | tags,
84 | operation_id,
85 | required_request_params,
86 | raw_path_params,
87 | description,
88 | security,
89 | deprecated,
90 | formats,
91 | ]
92 | end
93 |
94 | # @param [RSpec::ExampleGroups::*] context
95 | def request_response(context)
96 | request = ActionDispatch::Request.new(context.last_request.env)
97 | request.body.rewind if request.body.respond_to?(:rewind)
98 | response = ActionDispatch::TestResponse.new(*context.last_response.to_a)
99 |
100 | [request, response]
101 | end
102 |
103 | private
104 |
105 | def merge_openapi_metadata(metadata)
106 | collect_openapi_metadata(metadata).reduce({}, &:merge)
107 | end
108 |
109 | def collect_openapi_metadata(metadata)
110 | [].tap do |result|
111 | current = metadata
112 |
113 | while current
114 | [current[:example_group], current].each do |meta|
115 | result.unshift(meta[:openapi]) if meta&.dig(:openapi)
116 | end
117 |
118 | current = current[:parent_example_group]
119 | end
120 | end
121 | end
122 |
123 | def add_id(path, route)
124 | return path if route.params.empty?
125 |
126 | route.params.each_pair do |key, value|
127 | path = path.sub("/#{value}", "/:#{key}")
128 | end
129 |
130 | path
131 | end
132 |
133 | def add_openapi_id(path, route)
134 | return path if route.params.empty?
135 |
136 | route.params.each_pair do |key, value|
137 | path = path.sub("/#{value}", "/{#{key}}")
138 | end
139 |
140 | path
141 | end
142 | end
143 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/components_updater.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'hash_helper'
4 |
5 | class << RSpec::OpenAPI::ComponentsUpdater = Object.new
6 | # @param [Hash] base
7 | # @param [Hash] fresh
8 | def update!(base, fresh)
9 | # Top-level schema: Used as the body of request or response
10 | top_level_refs = paths_to_top_level_refs(base)
11 | return if top_level_refs.empty?
12 |
13 | fresh_schemas = build_fresh_schemas(top_level_refs, base, fresh)
14 |
15 | # Nested schema: References in Top-level schemas. May contain some top-level schema.
16 | generated_schema_names = fresh_schemas.keys
17 | nested_refs = find_non_top_level_nested_refs(base, generated_schema_names)
18 | nested_refs.each do |paths|
19 | # Slice between the parent name and the element before "$ref"
20 | # ["components", "schema", "Table", "properties", "database", "$ref"]
21 | # 0 1 2 ^....................^
22 | # ["components", "schema", "Table", "properties", "columns", "items", "$ref"]
23 | # 0 1 2 ^...............................^
24 | # ["components", "schema", "Table", "properties", "owner", "properties", "company", "$ref"]
25 | # 0 1 2 ^...........................................^
26 | needle = paths.reject { |path| path.is_a?(Integer) || path == :oneOf }
27 | needle = needle.slice(2, needle.size - 3)
28 | nested_schema = fresh_schemas.dig(*needle)
29 |
30 | # Skip if the property using $ref is not found in the parent schema. The property may be removed.
31 | next if nested_schema.nil?
32 |
33 | schema_name = base.dig(*paths)&.gsub('#/components/schemas/', '')&.to_sym
34 | fresh_schemas[schema_name] ||= {}
35 | RSpec::OpenAPI::SchemaMerger.merge!(fresh_schemas[schema_name], nested_schema)
36 | end
37 |
38 | RSpec::OpenAPI::SchemaMerger.merge!(base, { components: { schemas: fresh_schemas } })
39 | RSpec::OpenAPI::SchemaCleaner.cleanup_components_schemas!(base, { components: { schemas: fresh_schemas } })
40 | end
41 |
42 | private
43 |
44 | def build_fresh_schemas(references, base, fresh)
45 | references.inject({}) do |acc, paths|
46 | ref_link = dig_schema(base, paths)[:$ref]
47 | schema_name = ref_link.to_s.gsub('#/components/schemas/', '')
48 | schema_body = dig_schema(fresh, paths.reject { |path| path.is_a?(Integer) })
49 |
50 | RSpec::OpenAPI::SchemaMerger.merge!(acc, { schema_name => schema_body })
51 | end
52 | end
53 |
54 | def dig_schema(obj, paths)
55 | # Response code can be an integer
56 | paths = paths.map { |path| path.is_a?(Integer) ? path : path.to_sym }
57 | item_schema = obj.dig(*paths, :schema, :items)
58 | object_schema = obj.dig(*paths, :schema)
59 | one_of_schema = obj.dig(*paths.take(paths.size - 1), :schema, :oneOf, paths.last)
60 |
61 | item_schema || object_schema || one_of_schema
62 | end
63 |
64 | def paths_to_top_level_refs(base)
65 | request_bodies = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.requestBody.content.application/json')
66 | responses = RSpec::OpenAPI::HashHelper.matched_paths(base, 'paths.*.*.responses.*.content.application/json')
67 | (request_bodies + responses).flat_map do |paths|
68 | object_paths = find_object_refs(base, paths)
69 | one_of_paths = find_one_of_refs(base, paths)
70 |
71 | object_paths || one_of_paths || []
72 | end
73 | end
74 |
75 | def find_non_top_level_nested_refs(base, generated_names)
76 | nested_refs = [
77 | *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.$ref'),
78 | *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties.*.items.$ref'),
79 | *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'oneOf.*.$ref'),
80 | ]
81 | # Reject already-generated schemas to reduce unnecessary loop
82 | nested_refs.reject do |paths|
83 | ref_link = base.dig(*paths)
84 | schema_name = ref_link.gsub('#/components/schemas/', '')
85 | generated_names.include?(schema_name)
86 | end
87 | end
88 |
89 | def find_one_of_refs(base, paths)
90 | dig_schema(base, paths)&.dig(:oneOf)&.map&.with_index do |schema, index|
91 | paths + [index] if schema&.dig(:$ref)&.start_with?('#/components/schemas/')
92 | end&.compact
93 | end
94 |
95 | def find_object_refs(base, paths)
96 | [paths] if dig_schema(base, paths)&.dig(:$ref)&.start_with?('#/components/schemas/')
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/spec/requests/rails_smart_merge_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ENV['TZ'] ||= 'UTC'
4 | ENV['RAILS_ENV'] ||= 'test'
5 | ENV['OPENAPI_OUTPUT'] ||= 'json'
6 |
7 | require File.expand_path('../apps/rails/config/environment', __dir__)
8 | require 'rspec/rails'
9 |
10 | RSpec::OpenAPI.request_headers = %w[X-Authorization-Token]
11 | RSpec::OpenAPI.response_headers = %w[X-Cursor]
12 | RSpec::OpenAPI.path = File.expand_path("../apps/rails/doc/smart/openapi.#{ENV.fetch('OPENAPI_OUTPUT', nil)}", __dir__)
13 | RSpec::OpenAPI.comment = <<~COMMENT
14 | This file is auto-generated by rspec-openapi https://github.com/k0kubun/rspec-openapi
15 |
16 | When you write a spec in spec/requests, running the spec with `OPENAPI=1 rspec` will
17 | update this file automatically. You can also manually edit this file.
18 | COMMENT
19 | RSpec::OpenAPI.servers = [{ url: 'http://localhost:3000' }]
20 | RSpec::OpenAPI.security_schemes = {
21 | 'Scheme1' => {
22 | description: 'Authentication scheme',
23 | type: 'http',
24 | scheme: 'bearer',
25 | bearerFormat: 'JWT',
26 | },
27 | }
28 |
29 | RSpec::OpenAPI.info = {
30 | description: 'My beautiful API',
31 | license: {
32 | name: 'Apache 2.0',
33 | url: 'https://www.apache.org/licenses/LICENSE-2.0.html',
34 | },
35 | }
36 |
37 | RSpec::OpenAPI.formats_builder = ->(_example, key) { key.end_with?('_at') ? 'date-time' : nil }
38 |
39 | # Small subset of `rails_spec.rb` with slight changes
40 | RSpec.describe 'Tables', type: :request do
41 | describe '#index', openapi: { required_request_params: 'show_columns', operation_id: 'table-index' } do
42 | context it 'returns a list of tables' do
43 | it 'with flat query parameters' do
44 | # These new params replace them in old spec
45 | get '/tables', params: { page: '42', per: '10', show_columns: true },
46 | headers: { authorization: 'k0kubun', 'X-Authorization-Token': 'token' }
47 | response.set_header('X-Cursor', 100)
48 | expect(response.status).to eq(200)
49 | end
50 | end
51 |
52 | it 'does not return tables if unauthorized' do
53 | get '/tables'
54 | expect(response.status).to eq(401)
55 | end
56 | end
57 |
58 | describe '#show' do
59 | it 'returns a table with changes !!!' do
60 | get '/tables/1', headers: { authorization: 'k0kubun' }
61 | expect(response.status).to eq(200)
62 | end
63 | end
64 | end
65 |
66 | RSpec.describe 'Users', type: :request do
67 | describe '#create' do
68 | it 'accepts missing avatar_url' do
69 | post '/users', headers: { authorization: 'k0kubun', 'Content-Type': 'application/json' }, params: {
70 | name: 'alice',
71 | }.to_json
72 | expect(response.status).to eq(201)
73 | end
74 |
75 | it 'accepts nested object' do
76 | post '/users', headers: { authorization: 'k0kubun', 'Content-Type': 'application/json' }, params: {
77 | name: 'alice',
78 | foo: {
79 | bar: {
80 | baz: 42,
81 | },
82 | },
83 | }.to_json
84 | expect(response.status).to eq(201)
85 | end
86 |
87 | it 'accepts nested object where some fields are missing' do
88 | post '/users', headers: { authorization: 'k0kubun', 'Content-Type': 'application/json' }, params: {
89 | name: 'alice',
90 | foo: {
91 | bar: {},
92 | },
93 | }.to_json
94 | expect(response.status).to eq(201)
95 | end
96 |
97 | it 'can accept empty body' do
98 | post '/users', headers: { authorization: 'k0kubun', 'Content-Type': 'application/json' }, params: {}.to_json
99 | expect(response.status).to eq(201)
100 | end
101 |
102 | it 'returns an user' do
103 | post '/users', headers: { authorization: 'k0kubun', 'Content-Type': 'application/json' }, params: {
104 | name: 'alice',
105 | avatar_url: 'https://example.com/avatar.png',
106 | }.to_json
107 | expect(response.status).to eq(201)
108 | end
109 | end
110 |
111 | describe '#show' do
112 | it 'returns a user' do
113 | get '/users/1'
114 | expect(response.status).to eq(200)
115 | end
116 |
117 | it 'returns a user whose fields may be missing' do
118 | get '/users/2'
119 | expect(response.status).to eq(200)
120 | end
121 | end
122 |
123 | describe '#active' do
124 | it 'returns a boolean' do
125 | get '/users/1/active'
126 | expect(response.status).to eq(200)
127 | end
128 | end
129 | end
130 |
131 | RSpec.describe 'Pages', type: :request do
132 | describe '#get' do
133 | it 'return HTML' do
134 | get '/pages'
135 | expect(response.status).to eq(200)
136 | end
137 |
138 | it 'return no content' do
139 | get '/pages?head=1'
140 | expect(response.status).to eq(204)
141 | end
142 | end
143 | end
144 |
--------------------------------------------------------------------------------
/lib/rspec/openapi/schema_cleaner.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # For Ruby 3.0+
4 | require 'set'
5 |
6 | require_relative 'hash_helper'
7 |
8 | class << RSpec::OpenAPI::SchemaCleaner = Object.new
9 | # Cleanup the properties, of component schemas, that exists in the base but not in the spec.
10 | #
11 | # @param [Hash] base
12 | # @param [Hash] spec
13 | def cleanup_components_schemas!(base, spec)
14 | cleanup_hash!(base, spec, 'components.schemas.*')
15 | cleanup_hash!(base, spec, 'components.schemas.*.properties.*')
16 | end
17 |
18 | # Cleanup specific elements that exists in the base but not in the spec
19 | #
20 | # @param [Hash] base
21 | # @param [Hash] spec
22 | def cleanup!(base, spec)
23 | # cleanup URLs
24 | cleanup_hash!(base, spec, 'paths.*')
25 |
26 | # cleanup HTTP methods
27 | cleanup_hash!(base, spec, 'paths.*.*')
28 |
29 | # cleanup parameters
30 | cleanup_array!(base, spec, 'paths.*.*.parameters', %i[name in])
31 |
32 | # cleanup requestBody
33 | cleanup_hash!(base, spec, 'paths.*.*.requestBody.content.application/json.schema.properties.*')
34 | cleanup_hash!(base, spec, 'paths.*.*.requestBody.content.application/json.example.*')
35 |
36 | # cleanup responses
37 | cleanup_hash!(base, spec, 'paths.*.*.responses.*.content.application/json.schema.properties.*')
38 | cleanup_hash!(base, spec, 'paths.*.*.responses.*.content.application/json.example.*')
39 | base
40 | end
41 |
42 | def cleanup_conflicting_security_parameters!(base)
43 | security_schemes = base.dig(:components, :securitySchemes) || {}
44 |
45 | return if security_schemes.empty?
46 |
47 | paths_to_security_definitions = RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'paths', 'security')
48 |
49 | paths_to_security_definitions.each do |path|
50 | parent_path_definition = base.dig(*path.take(path.length - 1))
51 |
52 | security_schemes.each do |security_scheme_name, security_scheme|
53 | remove_parameters_conflicting_with_security_scheme!(
54 | parent_path_definition, security_scheme, security_scheme_name,
55 | )
56 | end
57 | end
58 | end
59 |
60 | def cleanup_empty_required_array!(base)
61 | paths_to_objects = [
62 | *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties'),
63 | *RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'paths', 'properties'),
64 | ]
65 | paths_to_objects.each do |path|
66 | parent = base.dig(*path.take(path.length - 1))
67 | # "required" array must not be present if empty
68 | parent.delete(:required) if parent[:required] && parent[:required].empty?
69 | end
70 | end
71 |
72 | private
73 |
74 | def remove_parameters_conflicting_with_security_scheme!(path_definition, security_scheme, security_scheme_name)
75 | return unless path_definition[:security]
76 | return unless path_definition[:parameters]
77 | return unless path_definition.dig(:security, 0).keys.include?(security_scheme_name)
78 |
79 | path_definition[:parameters].reject! do |parameter|
80 | parameter[:in] == security_scheme[:in] && # same location (ie. header)
81 | parameter[:name] == security_scheme[:name] # same name (ie. AUTHORIZATION)
82 | end
83 | path_definition.delete(:parameters) if path_definition[:parameters].empty?
84 | end
85 |
86 | def cleanup_array!(base, spec, selector, fields_for_identity = [])
87 | marshal = lambda do |obj|
88 | Marshal.dump(slice(obj, fields_for_identity))
89 | end
90 |
91 | RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
92 | target_array = base.dig(*paths)
93 | spec_array = spec.dig(*paths)
94 | next unless target_array.is_a?(Array) && spec_array.is_a?(Array)
95 |
96 | spec_identities = Set.new(spec_array.map(&marshal))
97 | target_array.select! { |e| spec_identities.include?(marshal.call(e)) }
98 | target_array.sort_by! { |param| fields_for_identity.map { |f| param[f] }.join('-') }
99 | # Keep the last duplicate to produce the result stably
100 | deduplicated = target_array.reverse.uniq { |param| slice(param, fields_for_identity) }.reverse
101 | target_array.replace(deduplicated)
102 | end
103 | base
104 | end
105 |
106 | def cleanup_hash!(base, spec, selector)
107 | RSpec::OpenAPI::HashHelper.matched_paths(base, selector).each do |paths|
108 | exist_in_base = !base.dig(*paths).nil?
109 | not_in_spec = spec.dig(*paths).nil?
110 | if exist_in_base && not_in_spec
111 | if paths.size == 1
112 | base.delete(paths.last)
113 | else
114 | parent_node = base.dig(*paths[0..-2])
115 | parent_node.delete(paths.last)
116 | end
117 | end
118 | end
119 | base
120 | end
121 |
122 | def slice(obj, fields_for_identity)
123 | if fields_for_identity.any?
124 | obj.slice(*fields_for_identity)
125 | else
126 | obj
127 | end
128 | end
129 | end
130 |
--------------------------------------------------------------------------------
/spec/apps/rails/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
19 | # config.require_master_key = true
20 |
21 | # Disable serving static files from the `/public` folder by default since
22 | # Apache or NGINX already handles this.
23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
24 |
25 | # Compress CSS using a preprocessor.
26 | # config.assets.css_compressor = :sass
27 |
28 | # Do not fallback to assets pipeline if a precompiled asset is missed.
29 | config.assets.compile = false
30 |
31 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
32 | # config.action_controller.asset_host = 'http://assets.example.com'
33 |
34 | # Specifies the header that your server uses for sending files.
35 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
36 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
37 |
38 | # Store uploaded files on the local file system (see config/storage.yml for options).
39 | config.active_storage.service = :local
40 |
41 | # Mount Action Cable outside main process or domain.
42 | # config.action_cable.mount_path = nil
43 | # config.action_cable.url = 'wss://example.com/cable'
44 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
45 |
46 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
47 | # config.force_ssl = true
48 |
49 | # Use the lowest log level to ensure availability of diagnostic information
50 | # when problems arise.
51 | config.log_level = :debug
52 |
53 | # Prepend all log lines with the following tags.
54 | config.log_tags = [:request_id]
55 |
56 | # Use a different cache store in production.
57 | # config.cache_store = :mem_cache_store
58 |
59 | # Use a real queuing backend for Active Job (and separate queues per environment).
60 | # config.active_job.queue_adapter = :resque
61 | # config.active_job.queue_name_prefix = "railsapp_production"
62 |
63 | config.action_mailer.perform_caching = false
64 |
65 | # Ignore bad email addresses and do not raise email delivery errors.
66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
67 | # config.action_mailer.raise_delivery_errors = false
68 |
69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
70 | # the I18n.default_locale when a translation cannot be found).
71 | config.i18n.fallbacks = true
72 |
73 | # Send deprecation notices to registered listeners.
74 | config.active_support.deprecation = :notify
75 |
76 | # Use default logging formatter so that PID and timestamp are not suppressed.
77 | config.log_formatter = ::Logger::Formatter.new
78 |
79 | # Use a different logger for distributed setups.
80 | # require 'syslog/logger'
81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
82 |
83 | if ENV["RAILS_LOG_TO_STDOUT"].present?
84 | logger = ActiveSupport::Logger.new(STDOUT)
85 | logger.formatter = config.log_formatter
86 | config.logger = ActiveSupport::TaggedLogging.new(logger)
87 | end
88 |
89 | # Do not dump schema after migrations.
90 | config.active_record.dump_schema_after_migration = false
91 |
92 | # Inserts middleware to perform automatic connection switching.
93 | # The `database_selector` hash is used to pass options to the DatabaseSelector
94 | # middleware. The `delay` is used to determine how long to wait after a write
95 | # to send a subsequent read to the primary.
96 | #
97 | # The `database_resolver` class is used by the middleware to determine which
98 | # database is appropriate to use based on the time delay.
99 | #
100 | # The `database_resolver_context` class is used by the middleware to set
101 | # timestamps for the last write to the primary. The resolver uses the context
102 | # class timestamps to determine how long to wait before reading from the
103 | # replica.
104 | #
105 | # By default Rails will store a last write timestamp in the session. The
106 | # DatabaseSelector middleware is designed as such you can define your own
107 | # strategy for connection switching and pass that into the middleware through
108 | # these configuration options.
109 | # config.active_record.database_selector = { delay: 2.seconds }
110 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
111 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
112 | end
113 |
--------------------------------------------------------------------------------
/spec/apps/rails/app/controllers/array_hashes_controller.rb:
--------------------------------------------------------------------------------
1 | class ArrayHashesController < ApplicationController
2 | def nullable
3 | response = {
4 | "users" => [
5 | {
6 | "label" => "John Doe",
7 | "value" => "john_doe",
8 | "admin" => true
9 | },
10 | {
11 | "label" => "Jane Doe",
12 | "value" => "jane_doe"
13 | },
14 | {
15 | "label" => nil,
16 | "value" => "invited",
17 | "invited" => true
18 | },
19 | ],
20 | }
21 | render json: response
22 | end
23 |
24 | def non_nullable
25 | response = {
26 | "users" => [
27 | {
28 | "label" => "Jane Doe",
29 | "value" => "jane_doe"
30 | },
31 | {
32 | "label" => "John Doe",
33 | "value" => "john_doe"
34 | }
35 | ],
36 | }
37 | render json: response
38 | end
39 |
40 | def nested
41 | response = {
42 | "fields" => [
43 | {
44 | "id" => "country_code",
45 | "options" => [
46 | {
47 | "id" => "us",
48 | "label" => "United States"
49 | },
50 | {
51 | "id" => "ca",
52 | "label" => "Canada"
53 | }
54 | ],
55 | "validations" => nil,
56 | "always_nil" => nil
57 |
58 | },
59 | {
60 | "id" => "region_id",
61 | "options" => [
62 | {
63 | "id" => 1,
64 | "label" => "New York"
65 | },
66 | {
67 | "id" => 2,
68 | "label" => "California"
69 | }
70 | ],
71 | "validations" => {
72 | "presence" => true
73 | },
74 | "always_nil" => nil
75 | }
76 | ]
77 | }
78 | render json: response
79 | end
80 |
81 | def empty_array
82 | response = {
83 | "items" => []
84 | }
85 | render json: response
86 | end
87 |
88 | def single_item
89 | response = {
90 | "items" => [
91 | {
92 | "id" => 1,
93 | "name" => "Item 1"
94 | }
95 | ]
96 | }
97 | render json: response
98 | end
99 |
100 | def non_hash_items
101 | response = {
102 | "items" => ["string1", "string2", "string3"]
103 | }
104 | render json: response
105 | end
106 |
107 | def nested_arrays
108 | response = {
109 | "items" => [
110 | {
111 | "id" => 1,
112 | "tags" => ["ruby", "rails"]
113 | },
114 | {
115 | "id" => 2,
116 | "tags" => ["python", "django"]
117 | },
118 | {
119 | "id" => 3,
120 | "tags" => ["javascript"]
121 | }
122 | ]
123 | }
124 | render json: response
125 | end
126 |
127 | def nested_objects
128 | response = {
129 | "items" => [
130 | {
131 | "id" => 1,
132 | "metadata" => {
133 | "author" => "Alice",
134 | "version" => "1.0"
135 | },
136 | "actions" => [
137 | {
138 | "label" => "Duplicate",
139 | "modal" => {
140 | "confirm_action" => {
141 | "label" => "Duplicate",
142 | "endpoint" => nil
143 | }
144 | }
145 | },
146 | {
147 | "label" => "Edit",
148 | },
149 | {
150 | "label" => "Something Else Again",
151 | "modal" => {
152 | "confirm_action" => {
153 | "label" => nil,
154 | "endpoint" => nil
155 | }
156 | }
157 | }
158 | ]
159 | },
160 | {
161 | "id" => 2,
162 | "metadata" => {
163 | "author" => "Bob",
164 | "version" => "2.0",
165 | "reviewed" => true
166 | },
167 | "actions" => [
168 | {
169 | "label" => "Duplicate",
170 | "modal" => {
171 | "confirm_action" => {
172 | "label" => "Duplicate",
173 | "endpoint" => nil
174 | }
175 | }
176 | },
177 | {
178 | "label" => "Edit",
179 | },
180 | {
181 | "label" => "Something Else Again",
182 | "modal" => {
183 | "confirm_action" => {
184 | "label" => nil,
185 | "endpoint" => nil
186 | }
187 | }
188 | }
189 | ]
190 | },
191 | {
192 | "id" => 3,
193 | "metadata" => {
194 | "author" => "Charlie"
195 | }
196 | }
197 | ]
198 | }
199 | render json: response
200 | end
201 |
202 | def mixed_types_nested
203 | response = {
204 | "items" => [
205 | {
206 | "id" => 1,
207 | "config" => {
208 | "port" => 8080,
209 | "host" => "localhost"
210 | },
211 | "form" => [
212 | {
213 | "value" => "John Doe",
214 | "options" => [
215 | {"label" => "John Doe", "value" => "john_doe"},
216 | {"label" => "Jane Doe", "value" => "jane_doe"}
217 | ]
218 | },
219 | {
220 | "value" => [],
221 | "options" => {
222 | "endpoint" => "some/endpoint"
223 | }
224 | },
225 | {
226 | "value" => nil,
227 | "options" => nil
228 | },
229 | ]
230 | },
231 | {
232 | "id" => 2,
233 | "config" => {
234 | "port" => "3000",
235 | "host" => "example.com",
236 | "ssl" => true
237 | },
238 | "form" => nil
239 | }
240 | ]
241 | }
242 | render json: response
243 | end
244 | end
245 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # THIS CHANGELOG IS DEPRECATED!!
2 |
3 | Refer https://github.com/exoego/rspec-openapi/releases instead.
4 |
5 | ## v0.12.0
6 |
7 | - feat: Initial support of complex schema with manually-added `oneOf`
8 | [#174](https://github.com/exoego/rspec-openapi/pull/174)
9 | - chore: Test with Ruby 3.3 and Rails 7.1.x
10 | [#169](https://github.com/exoego/rspec-openapi/pull/169)
11 |
12 | ## v0.11.0
13 | - feat: Allow path-based config overrides
14 | [#162](https://github.com/exoego/rspec-openapi/pull/162)
15 | - enhancement: Sort HTTP methods, response status codes, and contents lexicographically
16 | [#163](https://github.com/exoego/rspec-openapi/pull/163)
17 | - enhancement: Remove parameters that conflict with security schemas
18 | [#166](https://github.com/exoego/rspec-openapi/pull/166)
19 |
20 | ## v0.10.0
21 | - bugfix: Merge parameter data to preserve description in manually edited Openapi spec
22 | [#149](https://github.com/exoego/rspec-openapi/pull/149)
23 | - feat: Add ability to configure which path params to ignore
24 | [#150](https://github.com/exoego/rspec-openapi/pull/150)
25 | - feat: Add custom title
26 | [#147](https://github.com/exoego/rspec-openapi/pull/147)
27 | - feat: Add ability to define custom summary and tags builders
28 | [#148](https://github.com/exoego/rspec-openapi/pull/148)
29 | - enhancement: Sort paths lexicographically so the order of paths is more stable and predictable
30 | [#155](https://github.com/exoego/rspec-openapi/pull/155)
31 | - enhancement: requestBody should not merge requestBody from error examples
32 | [#154](https://github.com/exoego/rspec-openapi/pull/154)
33 |
34 | ## v0.9.0
35 | - bugfix: Fix engine path resolution
36 | [#113](https://github.com/exoego/rspec-openapi/pull/113)
37 | - bugfix: fix multiple uploaded files
38 | [#117](https://github.com/exoego/rspec-openapi/pull/117), [#126](https://github.com/exoego/rspec-openapi/pull/126)
39 | - feat: Add required_request_params to metadata
40 | [#114](https://github.com/exoego/rspec-openapi/pull/114)
41 | - bugfix(minitest):
42 | [#128](https://github.com/exoego/rspec-openapi/pull/128)
43 | - doc(minitest): Add instructions for minitest triggered yaml generation
44 | [#116](https://github.com/exoego/rspec-openapi/pull/116)
45 | - chore: Don't dump records into temporary file
46 | [#127](https://github.com/exoego/rspec-openapi/pull/127)
47 |
48 | ## v0.8.1
49 | - bugfix: Empty `required` array should not be present.
50 | [#111](https://github.com/exoego/rspec-openapi/pull/111)
51 |
52 | ## v0.8.0
53 | - Set `required` in request body and response body
54 | [#95](https://github.com/exoego/rspec-openapi/pull/95), [#98](https://github.com/exoego/rspec-openapi/pull/98)
55 | - Generate OpenAPI with minitest instead of RSpec
56 | [#90](https://github.com/exoego/rspec-openapi/pull/90)
57 | - Generate security schemas via RSpec::OpenAPI.security_schemes
58 | [#84](https://github.com/exoego/rspec-openapi/pull/84)
59 | - Bunch of refactorings
60 |
61 | ## v0.7.2
62 | - $ref enhancements: Support $ref in arbitrary depth
63 | [#82](https://github.com/k0kubun/rspec-openapi/pull/82)
64 |
65 | ## v0.7.1
66 | - $ref enhancements: Auto-generate components referenced in "items"
67 | [#80](https://github.com/k0kubun/rspec-openapi/pull/80)
68 | - Administration
69 | - Setup RuboCop
70 | [#73](https://github.com/k0kubun/rspec-openapi/pull/73)
71 | - Setup CodeQL
72 | [#73](https://github.com/k0kubun/rspec-openapi/pull/73)
73 | - Bump Rails v6.0.3.x to fix bundle failure
74 | [#72](https://github.com/k0kubun/rspec-openapi/pull/72)
75 |
76 | ## v0.7.0
77 | - Generate Response Headers
78 | [#69](https://github.com/k0kubun/rspec-openapi/pull/69)
79 | - Initial support for $ref
80 | [#67](https://github.com/k0kubun/rspec-openapi/pull/67)
81 | - Fixed an empty array is turned into nullable object wrongly
82 | [#70](https://github.com/k0kubun/rspec-openapi/pull/70)
83 |
84 | ## v0.6.1
85 |
86 | * Stabilize the order parameter objects and preserve newer examples
87 | [#59](https://github.com/k0kubun/rspec-openapi/pull/59)
88 |
89 | ## v0.6.0
90 |
91 | * Replace `RSpec::OpenAPI.server_urls` with `RSpec::OpenAPI.servers`
92 | [#60](https://github.com/k0kubun/rspec-openapi/pull/60)
93 |
94 | ## v0.5.1
95 |
96 | * Clarify the version requirement for actionpack
97 | [#62](https://github.com/k0kubun/rspec-openapi/pull/62)
98 |
99 | ## v0.5.0
100 |
101 | * Overwrite fields in an existing schema file instead of leaving all existing fields as is
102 | [#55](https://github.com/k0kubun/rspec-openapi/pull/55)
103 |
104 | ## v0.4.8
105 |
106 | * Fix a bug in nested parameters handling
107 | [#46](https://github.com/k0kubun/rspec-openapi/pull/46)
108 |
109 | ## v0.4.7
110 |
111 | * Add `info` config hash
112 | [#43](https://github.com/k0kubun/rspec-openapi/pull/43)
113 |
114 | ## v0.4.6
115 |
116 | * Fix "No route matched for" error in engine routes
117 | [#38](https://github.com/k0kubun/rspec-openapi/pull/38)
118 |
119 | ## v0.4.5
120 |
121 | * Fix linter issues for `tags` and `summary`
122 | [#40](https://github.com/k0kubun/rspec-openapi/pull/40)
123 |
124 | ## v0.4.4
125 |
126 | * De-duplicate parameters by a combination of `name` and `in`
127 | [#39](https://github.com/k0kubun/rspec-openapi/pull/39)
128 |
129 | ## v0.4.3
130 |
131 | * Allow customizing `schema`, `description`, and `tags` through `:openapi` metadata
132 | [#36](https://github.com/k0kubun/rspec-openapi/pull/36)
133 |
134 | ## v0.4.2
135 |
136 | * Allow using Proc as `RSpec::OpenAPI.path`
137 | [#35](https://github.com/k0kubun/rspec-openapi/pull/35)
138 |
139 | ## v0.4.1
140 |
141 | * Add `RSpec::OpenAPI.example_types` to hook types other than `type: :request`.
142 | [#32](https://github.com/k0kubun/rspec-openapi/pull/32)
143 |
144 | ## v0.4.0
145 |
146 | * Drop `RSpec::OpenAPI.output` introduced in v0.3.20
147 | * Guess whether it's JSON or not by the extension of `RSpec::OpenAPI.path`
148 |
149 | ## v0.3.20
150 |
151 | * Add `RSpec::OpenAPI.output` config to output JSON
152 | [#31](https://github.com/k0kubun/rspec-openapi/pull/31)
153 |
154 | ## v0.3.19
155 |
156 | * Add `server_urls` and `request_headers` configs
157 | [#30](https://github.com/k0kubun/rspec-openapi/pull/30)
158 |
159 | ## v0.3.18
160 |
161 | * Support nested query parameters
162 | [#29](https://github.com/k0kubun/rspec-openapi/pull/29)
163 |
164 | ## v0.3.17
165 |
166 | * Rescue NotImplementedError in the after suite hook as well
167 | [#28](https://github.com/k0kubun/rspec-openapi/pull/28)
168 |
169 | ## v0.3.16
170 |
171 | * Use `media_type` instead of `content_type` for Rails 6.1
172 | [#26](https://github.com/k0kubun/rspec-openapi/pull/26)
173 | * Avoid crashing the after suite hook when it fails to build schema
174 | [#27](https://github.com/k0kubun/rspec-openapi/pull/27)
175 |
176 | ## v0.3.15
177 |
178 | * Fix an error when Content-Disposition header is inline
179 | [#24](https://github.com/k0kubun/rspec-openapi/pull/24)
180 |
181 | ## v0.3.14
182 |
183 | * Avoid an error when an application calls `request.body.read`
184 | [#20](https://github.com/k0kubun/rspec-openapi/pull/20)
185 |
186 | ## v0.3.13
187 |
188 | * Avoid crashing when there's no request made in a spec
189 | [#19](https://github.com/k0kubun/rspec-openapi/pull/19)
190 |
191 | ## v0.3.12
192 |
193 | * Generate `nullable: true` for null fields in schema
194 | [#18](https://github.com/k0kubun/rspec-openapi/pull/18)
195 |
196 | ## v0.3.11
197 |
198 | * Show a filename as an `example` for `ActionDispatch::Http::UploadedFile`
199 | [#17](https://github.com/k0kubun/rspec-openapi/pull/17)
200 |
201 | ## v0.3.10
202 |
203 | * Add `info.version`
204 | [#16](https://github.com/k0kubun/rspec-openapi/pull/16)
205 |
206 | ## v0.3.9
207 |
208 | * Initial support for multipart/form-data
209 | [#12](https://github.com/k0kubun/rspec-openapi/pull/12)
210 |
211 | ## v0.3.8
212 |
213 | * Generate `type: 'number', format: 'float'` instead of `type: 'float'` for Float
214 | [#11](https://github.com/k0kubun/rspec-openapi/issues/11)
215 |
216 | ## v0.3.7
217 |
218 | * Classify tag names and remove controller names from summary in Rails
219 |
220 | ## v0.3.6
221 |
222 | * Fix documents generated by Rails engines
223 |
224 | ## v0.3.5
225 |
226 | * Support finding routes in Rails engines
227 |
228 | ## v0.3.4
229 |
230 | * Generate tags by controller names
231 | [#10](https://github.com/k0kubun/rspec-openapi/issues/10)
232 |
233 | ## v0.3.3
234 |
235 | * Avoid `JSON::ParserError` when a response body is no content
236 | [#9](https://github.com/k0kubun/rspec-openapi/issues/9)
237 |
238 | ## v0.3.2
239 |
240 | * Stop generating format as path parameters in Rails
241 | [#8](https://github.com/k0kubun/rspec-openapi/issues/8)
242 |
243 | ## v0.3.1
244 |
245 | * Add `RSpec::OpenAPI.description_builder` config to dynamically generate a description [experimental]
246 | [#6](https://github.com/k0kubun/rspec-openapi/issues/6)
247 |
248 | ## v0.3.0
249 |
250 | * Initial support of rack-test and non-Rails apps [#5](https://github.com/k0kubun/rspec-openapi/issues/5)
251 |
252 | ## v0.2.2
253 |
254 | * Allow disabling `example` by `RSpec::OpenAPI.enable_example = false`
255 |
256 | ## v0.2.1
257 |
258 | * Generate `example` of request body and path / query params
259 | [#4](https://github.com/k0kubun/rspec-openapi/issues/4)
260 | * Remove a wrapper param created by Rails in request body
261 | [#4](https://github.com/k0kubun/rspec-openapi/issues/4)
262 |
263 | ## v0.2.0
264 |
265 | * Generate `example` of response body [#3](https://github.com/k0kubun/rspec-openapi/issues/3)
266 |
267 | ## v0.1.5
268 |
269 | * Support detecting `float` type [#2](https://github.com/k0kubun/rspec-openapi/issues/2)
270 |
271 | ## v0.1.4
272 |
273 | * Avoid NoMethodError on nil Content-Type
274 | * Include a space between controller and action in summary
275 |
276 | ## v0.1.3
277 |
278 | * Add `RSpec::OpenAPI.comment` configuration
279 |
280 | ## v0.1.2
281 |
282 | * Generate `required: true` for path params [#1](https://github.com/k0kubun/rspec-openapi/issues/1)
283 |
284 | ## v0.1.1
285 |
286 | * Generate a path like `/{id}` instead of `/:id`
287 |
288 | ## v0.1.0
289 |
290 | * Initial release
291 |
--------------------------------------------------------------------------------
/spec/apps/rails/doc/smart/openapi.yaml:
--------------------------------------------------------------------------------
1 | # This file is auto-generated by rspec-openapi https://github.com/k0kubun/rspec-openapi
2 | #
3 | # When you write a spec in spec/requests, running the spec with `OPENAPI=1 rspec` will
4 | # update this file automatically. You can also manually edit this file.
5 | ---
6 | openapi: 3.0.3
7 | info:
8 | title: rspec-openapi
9 | version: 1.0.0
10 | description: My beautiful API
11 | license:
12 | name: Apache 2.0
13 | url: https://www.apache.org/licenses/LICENSE-2.0.html
14 | servers:
15 | - url: http://localhost:3000
16 | paths:
17 | "/no-such-path":
18 | get:
19 | summary: no such api
20 | parameters: []
21 | responses:
22 | '200':
23 | description: dummy
24 | content:
25 | application/json:
26 | schema:
27 | type: number
28 | example:
29 | - 1
30 | "/tables":
31 | get:
32 | summary: index
33 | tags:
34 | - Table
35 | parameters:
36 | - name: page
37 | in: query
38 | schema:
39 | type: integer
40 | example: 1
41 | - name: per
42 | in: query
43 | schema:
44 | type: integer
45 | example: 10
46 | - name: filter[name]
47 | in: query
48 | schema:
49 | type: object
50 | properties:
51 | name:
52 | type: string
53 | example:
54 | name: Example Table
55 | - name: filter[price]
56 | in: query
57 | schema:
58 | type: object
59 | properties:
60 | price:
61 | type: string
62 | example:
63 | price: '0'
64 | - name: X-Authorization-Token
65 | in: header
66 | required: true
67 | schema:
68 | type: string
69 | example: token
70 | responses:
71 | '200':
72 | description: with flat query parameters
73 | content:
74 | application/json:
75 | schema:
76 | type: array
77 | items:
78 | "$ref": "#/components/schemas/Table"
79 | example:
80 | - id: 1
81 | name: access
82 | description: logs
83 | database:
84 | id: 2
85 | name: production
86 | null_sample:
87 | storage_size: 12.3
88 | created_at: '2020-07-17T00:00:00+00:00'
89 | updated_at: '2020-07-17T00:00:00+00:00'
90 | '401':
91 | description: does not return tables if unauthorized
92 | content:
93 | application/json:
94 | schema:
95 | type: object
96 | properties:
97 | message:
98 | type: string
99 | no_such_field:
100 | type: string
101 | example: This field does not exist in rspec
102 | example:
103 | message: This field should be updated by rspec
104 | post:
105 | summary: This method should be removed!!
106 | tags:
107 | - Table
108 | requestBody:
109 | content:
110 | application/json:
111 | schema:
112 | type: object
113 | properties:
114 | name:
115 | type: string
116 | description:
117 | type: string
118 | database_id:
119 | type: integer
120 | no_such_field_request:
121 | type: boolean
122 | required:
123 | - name
124 | - description
125 | - database_id
126 | - no_such_field_request
127 | example:
128 | name: k0kubun
129 | description: description
130 | database_id: 2
131 | no_such_field_request: true
132 | responses:
133 | '201':
134 | description: returns a table
135 | content:
136 | application/json:
137 | schema:
138 | "$ref": "#/components/schemas/Table"
139 | example:
140 | id: 1
141 | name: access
142 | description: logs
143 | database:
144 | id: 2
145 | name: production
146 | null_sample:
147 | storage_size: 12.3
148 | created_at: '2020-07-17T00:00:00+00:00'
149 | updated_at: '2020-07-17T00:00:00+00:00'
150 | "/tables/{id}":
151 | get:
152 | summary: This filed should be updated by rspec
153 | tags:
154 | - Table
155 | parameters:
156 | - name: id
157 | in: path
158 | required: true
159 | schema:
160 | type: integer
161 | example: 1
162 | responses:
163 | '200':
164 | description: returns a table with changes !!!
165 | content:
166 | application/json:
167 | schema:
168 | type: object
169 | properties:
170 | id:
171 | type: integer
172 | name:
173 | type: string
174 | description:
175 | type: string
176 | database:
177 | discriminator:
178 | propertyName: name
179 | oneOf:
180 | - type: array
181 | items:
182 | type: object
183 | properties:
184 | name:
185 | type: string
186 | host:
187 | type: string
188 | port:
189 | type: integer
190 | user:
191 | type: string
192 | schema:
193 | type: string
194 | - type: object
195 | properties:
196 | host:
197 | type: string
198 | port:
199 | type: integer
200 | user:
201 | type: string
202 | schema:
203 | type: string
204 | required:
205 | - host
206 | - port
207 | - schema
208 | null_sample:
209 | nullable: true
210 | storage_size:
211 | type: number
212 | format: float
213 | no_such_field_response:
214 | type: boolean
215 | created_at:
216 | type: string
217 | updated_at:
218 | type: string
219 | example:
220 | id: 1
221 | name: access
222 | description: logs
223 | no_such_field_response: true
224 | database:
225 | id: 2
226 | name: production
227 | null_sample:
228 | storage_size: 12.3
229 | created_at: '2020-07-17T00:00:00+00:00'
230 | updated_at: '2020-07-17T00:00:00+00:00'
231 | "/users":
232 | post:
233 | summary: Create
234 | tags:
235 | - User
236 | requestBody:
237 | content:
238 | application/json:
239 | schema:
240 | oneOf:
241 | - "$ref": "#/components/schemas/PostUsersRequest"
242 | - type: string
243 | responses:
244 | '201':
245 | description: returns a user
246 | content:
247 | application/json:
248 | schema:
249 | "$ref": "#/components/schemas/User"
250 | "/users/{id}":
251 | get:
252 | responses:
253 | '200':
254 | description: returns a user
255 | content:
256 | application/json:
257 | schema:
258 | "$ref": "#/components/schemas/User"
259 | components:
260 | schemas:
261 | Table:
262 | type: object
263 | properties:
264 | id:
265 | type: integer
266 | # This field should exists in expected
267 | # name:
268 | # type: string
269 | this_field_should_not_exist_in_expected:
270 | type: string
271 | description:
272 | type: string
273 | database:
274 | "$ref": "#/components/schemas/Database"
275 | columns:
276 | type: array
277 | items:
278 | "$ref": "#/components/schemas/Column"
279 | null_sample:
280 | nullable: true
281 | storage_size:
282 | type: number
283 | format: float
284 | created_at:
285 | type: string
286 | updated_at:
287 | type: string
288 | Database:
289 | type: object
290 | description: 'this should be preserved'
291 | properties:
292 | id:
293 | type: integer
294 | description: 'this should be preserved'
295 | # This filed should exists in expected
296 | # name:
297 | # type: string
298 | this_field_should_not_exist_in_expected:
299 | type: string
300 | this_field_should_not_exist_in_expected_2:
301 | "$ref": "#/components/schemas/NoSuchSchemaFound"
302 | SchemaNotInUse:
303 | type: object
304 | description: 'should be deleted'
305 | properties:
306 | id:
307 | type: integer
308 | User:
309 | discriminator:
310 | propertyName: name
311 | oneOf:
312 | - type: object
313 | properties:
314 | name:
315 | type: string
316 | foo:
317 | type: string
318 | bar:
319 | type: string
320 | baz:
321 | type: string
322 | quux:
323 | type: string
324 | - type: object
325 | properties:
326 | name:
327 | type: string
328 | relations:
329 | type: object
330 | properties:
331 | avatar:
332 | "$ref": "#/components/schemas/Avatar"
333 | pets:
334 | type: array
335 | items:
336 | "$ref": "#/components/schemas/Pet"
337 |
--------------------------------------------------------------------------------