1. Install the Gem
Add the following gem to your Gemfile
:
gem 'dry-validation'
2. Create a Concern for Schema Validation
Create a concern to handle schema validation: app/controllers/concerns/request_schema_validable.rb
.
# frozen_string_literal: true
module RequestSchemaValidable
extend ActiveSupport::Concern
included do
# before_action :required_schema_validation!
rescue_from RequestSchemaError, with: :rescue_schema_validation_error
end
def required_schema_validation!
return true if required_schema.nil? || schema_params.blank?
result = required_schema.call(schema_params)
return true if result.success?
error_message = result.errors.to_h.to_json
raise RequestSchemaError, error_message
end
def schema_params
request.params
end
def rescue_schema_validation_error(ex)
payload = JSON.parse(ex.message)
render json: payload, status: :unprocessable_entity
end
# override this to your own schema in the controller
def required_schema
nil
end
end
3. Include the Concern in the Application Controller
Include the validation concern in your root controller: app/controllers/application_controller.rb
.
# frozen_string_literal: true
class ApplicationController < ActionController::Base
include RequestSchemaValidable
end
4. Use the Validator in Specific Controllers
For example, in app/controllers/users/sessions_controller.rb
, use the request validator:
# frozen_string_literal: true
module Users
class SessionsController < ApplicationController
def required_schema
OidcLogoutSchema
end
end
end
5. Create a Custom Error for Schema Validation
Define a custom error class for handling schema validation errors: app/errors/request_schema_error.rb
.
# frozen_string_literal: true
class RequestSchemaError < StandardError
end
6. Create a Base Class for Schema Validation
Encapsulate validation logic in a parent class: app/request_schemas/application_request_schema.rb
.
# frozen_string_literal: true
class ApplicationRequestSchema < Dry::Validation::Contract
# @return Dry::Validation::Result
def self.validate(options)
new.call(options)
end
# @return Dry::Validation::Result
def self.call(options)
new.call(options)
end
end
7. Create a Specific Schema for Validating Requests
Define the schema to validate your request, for example: app/request_schemas/oidc_logout_schema.rb
.
# frozen_string_literal: true
class OidcLogoutSchema < ApplicationRequestSchema
# params schema
params do
optional(:client_id).filled(:string)
optional(:id_token_hint).value(:string)
optional(:logout_sid).value(:string)
optional(:post_logout_redirect_uri).value(:string)
end
rule(:id_token_hint) do
key.failure('is missing') if values[:id_token_hint].blank? && values[:logout_sid].blank?
end
rule(:client_id) do
key.failure('is missing') if values[:id_token_hint].blank? && values[:logout_sid].present? && values[:client_id].blank?
end
rule(:post_logout_redirect_uri) do
error_message = 'is missing'
error_url = 'must be a valid url'
value = values[:post_logout_redirect_uri]
if value.blank?
key.failure(error_message)
else
uri = URI.parse(value)
valid = uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
key.failure(error_url) unless valid
end
rescue URI::InvalidURIError
key.failure(error_url)
end
end
8. Write RSpec Tests for the Schema
Create a spec file to test your validation logic: spec/request_schemas/oidc_logout_schema_spec.rb
.
require 'rails_helper'
RSpec.describe OidcLogoutSchema do
let(:oauth_app) { create(:oauth_application) }
let(:id_token_hint) { SecureRandom.hex(32) }
let(:error_message) { 'must be filled' }
let(:error_url) { 'must be a valid url' }
let(:valid_attrs) do
{
client_id: oauth_app.uid,
post_logout_redirect_uri: oauth_app.redirect_uris.first,
id_token_hint: id_token_hint
}
end
describe 'with valid attributes' do
it 'validate successfully' do
result = described_class.validate(valid_attrs)
expect(result.success?).to be_truthy
expect(result.errors).to be_blank
end
end
describe 'with invalid attributes' do
it 'requires client_id' do
result = described_class.validate(valid_attrs.merge(client_id: ''))
errors = result.errors.to_h
expect(result.success?).to be_falsey
expect(errors.keys.count).to eq 1
expect(errors[:client_id]).to match(['must be filled'])
end
context 'logout_sid blank' do
it 'requires id_token_hint' do
result = described_class.validate(valid_attrs.merge(id_token_hint: ''))
errors = result.errors.to_h
expect(result.success?).to be_falsey
expect(errors.keys.count).to eq 1
expect(errors[:id_token_hint]).to match(['is missing'])
end
end
context 'logout_sid present' do
it 'does not requires id_token_hint' do
result = described_class.validate(valid_attrs.merge(id_token_hint: '', logout_sid: 'anything'))
errors = result.errors.to_h
expect(result.success?).to be_truthy
expect(errors).to be_blank
end
end
it 'requires post_logout_redirect_uri to be present' do
result = described_class.validate(valid_attrs.merge(post_logout_redirect_uri: ''))
errors = result.errors.to_h
expect(result.success?).to be_falsey
expect(errors.keys.count).to eq 1
expect(errors[:post_logout_redirect_uri]).to match(['is missing'])
end
it 'requires post_logout_redirect_uri to be a valid uri format' do
result = described_class.validate(valid_attrs.merge(post_logout_redirect_uri: 'ftp://jo.2883.39'))
errors = result.errors.to_h
expect(result.success?).to be_falsey
expect(errors.keys.count).to eq 1
expect(errors[:post_logout_redirect_uri]).to match([error_url])
end
end
end
This structured approach enables you to efficiently integrate dry-validation
into your Rails project, allowing for modular and reusable validation schemas.