diff --git a/GemFile b/GemFile index bcf3166..e7aaf87 100644 --- a/GemFile +++ b/GemFile @@ -82,4 +82,6 @@ gem 'pagy', '~> 6.0' gem 'devise' -gem 'cancancan' \ No newline at end of file +gem 'cancancan' + +gem 'rswag' \ No newline at end of file diff --git a/GemFile.lock b/GemFile.lock index 0d6d1f6..8c7be83 100644 --- a/GemFile.lock +++ b/GemFile.lock @@ -117,6 +117,8 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.6.3) + json-schema (3.0.0) + addressable (>= 2.8) loofah (2.19.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) @@ -211,6 +213,20 @@ GEM rspec-mocks (~> 3.11) rspec-support (~> 3.11) rspec-support (3.12.0) + rswag (2.8.0) + rswag-api (= 2.8.0) + rswag-specs (= 2.8.0) + rswag-ui (= 2.8.0) + rswag-api (2.8.0) + railties (>= 3.1, < 7.1) + rswag-specs (2.8.0) + activesupport (>= 3.1, < 7.1) + json-schema (>= 2.2, < 4.0) + railties (>= 3.1, < 7.1) + rspec-core (>= 2.14) + rswag-ui (2.8.0) + actionpack (>= 3.1, < 7.1) + railties (>= 3.1, < 7.1) rubocop (1.43.0) json (~> 2.3) parallel (~> 1.10) @@ -286,6 +302,7 @@ DEPENDENCIES rails (~> 7.0.4) rails-controller-testing rspec-rails + rswag rubocop (>= 1.0, < 2.0) selenium-webdriver sprockets-rails diff --git a/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb new file mode 100644 index 0000000..4d72f68 --- /dev/null +++ b/config/initializers/rswag_api.rb @@ -0,0 +1,14 @@ +Rswag::Api.configure do |c| + + # Specify a root folder where Swagger JSON files are located + # This is used by the Swagger middleware to serve requests for API descriptions + # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure + # that it's configured to generate files in the same folder + c.swagger_root = Rails.root.to_s + '/swagger' + + # Inject a lambda function to alter the returned Swagger prior to serialization + # The function will have access to the rack env for the current request + # For example, you could leverage this to dynamically assign the "host" property + # + #c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } +end diff --git a/config/initializers/rswag_ui.rb b/config/initializers/rswag_ui.rb new file mode 100644 index 0000000..0a768c1 --- /dev/null +++ b/config/initializers/rswag_ui.rb @@ -0,0 +1,16 @@ +Rswag::Ui.configure do |c| + + # List the Swagger endpoints that you want to be documented through the + # swagger-ui. The first parameter is the path (absolute or relative to the UI + # host) to the corresponding endpoint and the second is a title that will be + # displayed in the document selector. + # NOTE: If you're using rspec-api to expose Swagger files + # (under swagger_root) as JSON or YAML endpoints, then the list below should + # correspond to the relative paths for those endpoints. + + c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs' + + # Add Basic Auth in case your API is private + # c.basic_auth_enabled = true + # c.basic_auth_credentials 'username', 'password' +end diff --git a/config/routes.rb b/config/routes.rb index 7498c2a..7f95739 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,6 @@ Rails.application.routes.draw do + mount Rswag::Ui::Engine => '/api-docs' + mount Rswag::Api::Engine => '/api-docs' #devise root devise_for :users devise_scope :user do diff --git a/spec/requests/api/v1/comments_spec.rb b/spec/requests/api/v1/comments_spec.rb new file mode 100644 index 0000000..e328118 --- /dev/null +++ b/spec/requests/api/v1/comments_spec.rb @@ -0,0 +1,163 @@ +require 'swagger_helper' + +RSpec.describe 'api/v1/comments', type: :request do + + path '/api/v1/users/{user_id}/posts/{post_id}/comments' do + # You'll want to customize the parameter types... + parameter name: 'user_id', in: :path, type: :string, description: 'user_id' + parameter name: 'post_id', in: :path, type: :string, description: 'post_id' + + get('list comments') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:post_id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + post('create comment') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:post_id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/v1/users/{user_id}/posts/{post_id}/comments/new' do + # You'll want to customize the parameter types... + parameter name: 'user_id', in: :path, type: :string, description: 'user_id' + parameter name: 'post_id', in: :path, type: :string, description: 'post_id' + + get('new comment') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:post_id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/v1/users/{user_id}/posts/{post_id}/comments/{id}/edit' do + # You'll want to customize the parameter types... + parameter name: 'user_id', in: :path, type: :string, description: 'user_id' + parameter name: 'post_id', in: :path, type: :string, description: 'post_id' + parameter name: 'id', in: :path, type: :string, description: 'id' + + get('edit comment') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:post_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/v1/users/{user_id}/posts/{post_id}/comments/{id}' do + # You'll want to customize the parameter types... + parameter name: 'user_id', in: :path, type: :string, description: 'user_id' + parameter name: 'post_id', in: :path, type: :string, description: 'post_id' + parameter name: 'id', in: :path, type: :string, description: 'id' + + get('show comment') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:post_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + patch('update comment') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:post_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + put('update comment') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:post_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + delete('delete comment') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:post_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end +end diff --git a/spec/requests/api/v1/posts_spec.rb b/spec/requests/api/v1/posts_spec.rb new file mode 100644 index 0000000..5f2b09d --- /dev/null +++ b/spec/requests/api/v1/posts_spec.rb @@ -0,0 +1,151 @@ +require 'swagger_helper' + +RSpec.describe 'api/v1/posts', type: :request do + + path '/api/v1/users/{user_id}/posts' do + # You'll want to customize the parameter types... + parameter name: 'user_id', in: :path, type: :string, description: 'user_id' + + get('list posts') do + response(200, 'successful') do + let(:user_id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + post('create post') do + response(200, 'successful') do + let(:user_id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/v1/users/{user_id}/posts/new' do + # You'll want to customize the parameter types... + parameter name: 'user_id', in: :path, type: :string, description: 'user_id' + + get('new post') do + response(200, 'successful') do + let(:user_id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/v1/users/{user_id}/posts/{id}/edit' do + # You'll want to customize the parameter types... + parameter name: 'user_id', in: :path, type: :string, description: 'user_id' + parameter name: 'id', in: :path, type: :string, description: 'id' + + get('edit post') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/v1/users/{user_id}/posts/{id}' do + # You'll want to customize the parameter types... + parameter name: 'user_id', in: :path, type: :string, description: 'user_id' + parameter name: 'id', in: :path, type: :string, description: 'id' + + get('show post') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + patch('update post') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + put('update post') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + delete('delete post') do + response(200, 'successful') do + let(:user_id) { '123' } + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end +end diff --git a/spec/requests/api/v1/users_spec.rb b/spec/requests/api/v1/users_spec.rb new file mode 100644 index 0000000..19a8dd1 --- /dev/null +++ b/spec/requests/api/v1/users_spec.rb @@ -0,0 +1,137 @@ +require 'swagger_helper' + +RSpec.describe 'api/v1/users', type: :request do + + path '/api/v1/users' do + + get('list users') do + response(200, 'successful') do + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + post('create user') do + response(200, 'successful') do + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/v1/users/new' do + + get('new user') do + response(200, 'successful') do + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/v1/users/{id}/edit' do + # You'll want to customize the parameter types... + parameter name: 'id', in: :path, type: :string, description: 'id' + + get('edit user') do + response(200, 'successful') do + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/v1/users/{id}' do + # You'll want to customize the parameter types... + parameter name: 'id', in: :path, type: :string, description: 'id' + + get('show user') do + response(200, 'successful') do + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + patch('update user') do + response(200, 'successful') do + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + put('update user') do + response(200, 'successful') do + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + delete('delete user') do + response(200, 'successful') do + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end +end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb new file mode 100644 index 0000000..562397f --- /dev/null +++ b/spec/swagger_helper.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.configure do |config| + # Specify a root folder where Swagger JSON files are generated + # NOTE: If you're using the rswag-api to serve API descriptions, you'll need + # to ensure that it's configured to serve Swagger from the same folder + config.swagger_root = Rails.root.join('swagger').to_s + + # Define one or more Swagger documents and provide global metadata for each one + # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will + # be generated at the provided relative path under swagger_root + # By default, the operations defined in spec files are added to the first + # document below. You can override this behavior by adding a swagger_doc tag to the + # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' + config.swagger_docs = { + 'v1/swagger.yaml' => { + openapi: '3.0.1', + info: { + title: 'API V1', + version: 'v1' + }, + paths: {}, + servers: [ + { + url: 'https://{defaultHost}', + variables: { + defaultHost: { + default: '127.0.0.1:3000/' + } + } + } + ] + } + } + + # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. + # The swagger_docs configuration option has the filename including format in + # the key, this may want to be changed to avoid putting yaml in json files. + # Defaults to json. Accepts ':json' and ':yaml'. + config.swagger_format = :yaml +end