Skip to content

Commit

Permalink
feat: add API docs
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasleger committed Dec 12, 2024
1 parent d6f1a11 commit 549698f
Show file tree
Hide file tree
Showing 19 changed files with 485 additions and 61 deletions.
10 changes: 10 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
inherit_from: .rubocop_todo.yml

inherit_gem:
rswag-specs: .rubocop_rspec_alias_config.yml

require:
- rubocop-rspec
- rubocop-rails
Expand All @@ -19,3 +22,10 @@ Style/Documentation:
Style/StringLiterals:
Enabled: true
EnforcedStyle: "double_quotes"

# API Docs

RSpec/NestedGroups:
Enabled: true
Exclude:
- 'spec/**/*_doc_spec.rb'
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ gem "active_storage_validations" # Validate ActiveStorage attachments
gem "good_job" # Postgres-backed job queue
gem "mime-types"
gem "pdf-reader"
gem "rswag-api" # Serves the generated Swagger documentation
gem "rswag-ui" # Provides the Swagger UI interface
gem "sib-api-v3-sdk", require: false # Brevo (ex Sendinblue) API

group :development, :test do
Expand All @@ -68,6 +70,7 @@ group :development, :test do
gem "factory_bot_rails"
gem "pry-rails"
gem "rspec-rails"
gem "rswag-specs" # Allows API documentation via specs
end

group :test do
Expand Down
17 changes: 17 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ GEM
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.8.2)
json-schema (5.1.1)
addressable (~> 2.8)
bigdecimal (~> 3.1)
language_server-protocol (3.17.0.3)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
Expand Down Expand Up @@ -358,6 +361,17 @@ GEM
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.2)
rswag-api (2.16.0)
activesupport (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1)
rswag-specs (2.16.0)
activesupport (>= 5.2, < 8.1)
json-schema (>= 2.2, < 6.0)
railties (>= 5.2, < 8.1)
rspec-core (>= 2.14)
rswag-ui (2.16.0)
actionpack (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1)
rubocop (1.68.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
Expand Down Expand Up @@ -478,6 +492,9 @@ DEPENDENCIES
rails
rspec
rspec-rails
rswag-api
rswag-specs
rswag-ui
rubocop
rubocop-capybara
rubocop-factory_bot
Expand Down
24 changes: 3 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ composants DSFR](https://github.com/betagouv/dsfr-view-components)
* Sentry pour monitorer et être alerté en cas d'erreur ;
* Matomo pour mesurer et comprendre l'usage via des analytics ;
* RSpec comme framework de tests ;
* Rswag comme outil de documentation au format Swagger/ OpenAPI de l'API à travers des tests ;
* Cucumber et Capybara pour les tests BDD ;
* Rubocop (RSpec et Rails) pour le linting ;
* Docker pour avoir un environnement de développement.
Expand All @@ -35,27 +36,8 @@ Les fichiers devis sont traités par le `QuoteChecksController` qui les envoient

## API

via header `Accept` à la valeur `application/json` pour forcer le retour au format JSON et non classique HTML

- GET `/profils` pour lister les profils disponibles
- POST `/[:profil]/devis/verifier` avec paramètre `quote_file` contenant le fichier
le type de retour avec erreurs retournées est:
```
{
"valid": false,
"errors": [
"file_reading_error",
"devis_manquant",
"pro_raison_sociale_manquant",
"pro_forme_juridique_manquant",
"tva_manquant",
"capital_manquant",
"siret_manquant",
"client_prenom_manquant",
"client_nom_manquant"
]
}
```
- au format REST JSON
- voir fichier de documentation de l'API au format OpenAPI Swagger et interface bac à sable interractif sur `/api-docs`

## Démarrage

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/profiles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module V1
# Controller for Profiles API
class ProfilesController < BaseController
def index
render json: QuoteCheck::PROFILES
render json: { data: QuoteCheck::PROFILES }
end
end
end
Expand Down
33 changes: 22 additions & 11 deletions app/controllers/api/v1/quote_checks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class QuoteChecksController < BaseController
before_action :quote_check, only: %i[show]

def show
# Force to use async way by using show to get other fields
render json: quote_check_json
end

Expand All @@ -23,11 +24,13 @@ def create
upload_file.tempfile, upload_file.original_filename, quote_check_params[:profile]
)
@quote_check = quote_check_service.quote_check
@quote_check = quote_check_service.check # Might be time consuming, TODO: move to background job is needed

# @quote_check = quote_check_service.check # Might be time consuming, TODO: move to background job is needed
QuoteCheckCheckJob.perform_later(@quote_check.id)

QuoteCheckMailer.created(@quote_check).deliver_later

render json: quote_check_json(@quote_check)
render json: quote_check_json(@quote_check), status: :created
end
# rubocop:enable Metrics/MethodLength

Expand All @@ -38,20 +41,28 @@ def quote_check
end

def quote_check_params
params.require(:quote_check).permit(:file, :profile)
params.permit(:file, :profile)
end

# rubocop:disable Metrics/MethodLength
def quote_check_json(quote_check_provided = nil)
object = quote_check_provided || quote_check
object.attributes.merge({
status: object.status,
valid: object.quote_valid?,
errors: object.validation_errors,
error_messages: object.validation_errors&.index_with do |error_key|
I18n.t("quote_validator.errors.#{error_key}")
end
})
json_hash = object.attributes.merge({ # Warning: attributes has stringifed keys, so use it too
"status" => object.status,
"valid" => object.quote_valid?,
"errors" => object.validation_errors,
"error_messages" => object.validation_errors&.index_with do |error_key|
I18n.t("quote_validator.errors.#{error_key}")
end
})
return json_hash if Rails.env.development?

json_hash.slice(
"id", "status", "profile",
"valid", "errors", "error_messages"
)
end
# rubocop:enable Metrics/MethodLength
end
end
end
2 changes: 1 addition & 1 deletion app/jobs/quote_check_check_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ class QuoteCheckCheckJob < ApplicationJob

def perform(quote_check_id)
quote_check = QuoteCheck.find(quote_check_id)
QuoteCheckCheckService.new(quote_check).check
QuoteCheckService.new(quote_check).check
end
end
2 changes: 2 additions & 0 deletions app/models/quote_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
class QuoteCheck < ApplicationRecord
belongs_to :file, class_name: "QuoteFile"

STATUSES = %w[pending valid invalid].freeze

PROFILES = %w[artisan particulier mandataire conseiller].freeze
validates :profile, presence: true, inclusion: { in: PROFILES }

Expand Down
2 changes: 1 addition & 1 deletion app/services/quote_check_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class QuoteCheckService
attr_reader :quote_check

def initialize(tempfile_or_quote_check, filename, profile)
def initialize(tempfile_or_quote_check, filename = nil, profile = nil)
@quote_check = if tempfile_or_quote_check.is_a?(QuoteCheck)
tempfile_or_quote_check
else
Expand Down
4 changes: 4 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,9 @@ class Application < Rails::Application
config.action_mailer.default_url_options = { host: ENV.fetch("APPLICATION_HOST", nil) }

config.application_name = "Mon Devis Sans Oublis"

config.openapi_file = lambda { |version|
"#{config.application_name.parameterize}_api_#{version.downcase}_swagger.yaml"
}
end
end
15 changes: 15 additions & 0 deletions config/initializers/rswag_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

Rswag::Api.configure do |config|
# 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
config.openapi_root = Rails.root.join("swagger").to_s

# 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
#
# config.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
end
18 changes: 18 additions & 0 deletions config/initializers/rswag_ui.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

Rswag::Ui.configure do |config|
# 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 openapi_root) as JSON or YAML endpoints, then the list below should
# correspond to the relative paths for those endpoints.

config.swagger_endpoint "/api-docs/v1/#{Rails.application.config.openapi_file.call('v1')}",
"#{Rails.application.config.application_name} API V1 Documentation"

# Add Basic Auth in case your API is private
# config.basic_auth_enabled = true
# config.basic_auth_credentials 'username', 'password'
end
3 changes: 3 additions & 0 deletions config/routes/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
resources :quote_checks, only: %i[create show]
end
end

mount Rswag::Api::Engine => "/api-docs"
mount Rswag::Ui::Engine => "/api-docs"
end
24 changes: 24 additions & 0 deletions spec/requests/api/v1/profiles_doc_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require "swagger_helper"

describe "Profiles API" do
path "/profiles" do
get "Récupérer les profils disponibles" do
tags "Profils"
produces "application/json"

response "200", "liste des profiles" do
schema type: :object,
properties: {
data: {
type: "array",
data: { type: "#/components/schemas/profile" }
}
},
required: ["data"]
run_test!
end
end
end
end
2 changes: 1 addition & 1 deletion spec/requests/api/v1/profiles_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

it "returns a complete response" do
get api_v1_profiles_url
expect(json).to include(*QuoteCheck::PROFILES)
expect(json.fetch("data")).to include(*QuoteCheck::PROFILES)
end
end
end
67 changes: 67 additions & 0 deletions spec/requests/api/v1/quote_checks_doc_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

require "swagger_helper"

describe "Devis API" do
path "/quote_checks" do
# TODO: i18n?
post "Téléverser un devis" do
tags "Devis"
# TODO: security [ basic_auth: [] ]
consumes "multipart/form-data"
produces "application/json"

parameter name: :file, in: :formData, schema: {
type: :string,
format: :binary
}, required: true
parameter name: :profile, in: :formData, schema: {
"$ref" => "#/components/schemas/profile"
}, required: true

response "201", "Devis téléversé" do
schema "$ref" => "#/components/schemas/quote_check"
description "Au retour le devis a été téléversé avec succès.
Mais vérifiez selon le statut si le devis a été déjà analysé ou non.
Il peut contenir des erreurs dès le téléversement.
Si le statut est 'pending', cela signifie que l'analyse est encore en cours.
Et qu'il faut boucler sur l'appel /quote_check/:id pour récupérer le devis à jour.".gsub("\n", "<br>")

let(:file) { fixture_file_upload("quote_files/Devis_test.pdf") }
let(:profile) { "artisan" }

run_test!
end

response "422", "invalid request" do
let(:file) { fixture_file_upload("quote_files/Devis_test.pdf") }
let(:profile) { "blabla" }

run_test!
end
end
end

path "/quote_checks/{id}" do
get "Récupérer un Devis" do
tags "Devis"
consumes "application/json"
produces "application/json"
parameter name: :id, in: :path, type: :string, required: true

response "200", "Devis trouvé" do
schema "$ref" => "#/components/schemas/quote_check"

let(:id) { create(:quote_check).id }

run_test!
end

response "404", "Devis non trouvé" do
let(:id) { SecureRandom.uuid }

run_test!
end
end
end
end
Loading

0 comments on commit 549698f

Please sign in to comment.