Tag: API

Dry-Validation as a schema validation layer for Ruby on Rails API

Legacy code is never the way we would like it to be

There are days, when you don't get to write your new shiny application using Grape and JSON-API. Life would be much easier, if we could always start from the beginning. Unfortunately we can't and one of the things that make some developers more valuable that others is their ability to adapt. The app might be outdated, it might have abandoned gems, but if you're able to introduce new concepts to it (without breaking it and having to rewrite everything), you should be able to overcome any other difficulties.

ActiveRecord::Validations is not the way to go

If you use Rails, then probably you use ActiveRecord::Validations as the main validation layer. Model validations weren't designed as a protection layer for ensuring incoming data schema consistency (and there are many more reasons why you should stop using them).

External world and its data won't always resemble your internal application structure. Instead of trying to adapt the code-base and fit it into the world, try making the world as much constant and predictable as it can be.

One of the best things in that matter is to ensure that any endpoint input data is as strict as it can be. You can do quite a lot, before it gets to your ActiveRecord models or hits any business logic.

Dry-Validation - different approach to solve different problems

Unlike other, well known, validation solutions in Ruby, dry-validation takes a different approach and focuses a lot on explicitness, clarity and precision of validation logic. It is designed to work with any data input, whether it’s a simple hash, an array or a complex object with deeply nested data.

Dry-validation is a library that can help you protect your typical Rails API endpoints better. You will never control what you will receive, but you can always make sure that it won't reach your business.

Here's a basic example of how it works (assuming we validate a single hash):

BeaconSchema = Dry::Validation.Schema do
  UUID_REGEXP = /\A([a-z|0-9]){32}\z/
  PROXIMITY_ZONES = %w(
    immediate
    near
    far
    unknown
  )

  required(:uuid).filled(:str?, size?: 32, format?: UUID_REGEXP)
  required(:rssi).filled(:int?, lteq?: -26, gteq?: -100)
  required(:major).filled(:int?, gteq?: 0, lteq?: 65535)
  required(:minor).filled(:int?, gteq?: 0, lteq?: 65535)
  required(:proximity_zone).filled(:str?, included_in?: PROXIMITY_ZONES)
  required(:distance) { filled? & ( int? | float? ) & gteq?(0) & lteq?(100)  }
end

data = {}
validation_result = BeaconSchema.call(data)
validation_result.errors #=> {:uuid=>["is missing"], :rssi=>["is missing"], ... }

The validation result object is really similar to the validation result of ActiveRecord::Validation in terms of usability.

Dry Ruby on Rails API

Integrating dry-validation with your Ruby on Rails API endpoints can give you multiple benefits, including:

  • No more strong parameters (they will become useless when you cover all the endpoints with dry-validation)
  • Schemas testing in isolation (not in the controller request - response flow)
  • Model validations that focus only on the core details (without the "this data is from form and this from the api" dumb cases)
  • Less coupling in your business logic
  • Safer extending and replacing of endpoints
  • More DRY
  • Nested structures validation

Non-obstructive data schema for a controller layer

There are several approaches you can take when implementing schema validation for your API endpoints. One that I find useful especially in legacy projects is the #before_action approach. Regardless whether #before_action is (or is not) an anti-pattern, in this case, it can be used to provide non-obstructive schema and data validation that does not interact with other layers (like services, models, etc.).

To implement such protection, you only need a couple lines of code):

class Api::BaseController < ApplicationController
  # Make sure that request data meets schema requirements
  before_action :ensure_schema!

  # Accessor for validation results hash
  attr_reader :validation_result
  # Schema assigned on a class level
  class_attribute :schema

  private

  def ensure_schema!
    @validation_result = self.class.schema.call(params)
    return if @validation_result.success?

    render json: @validation_result.errors.to_json, status: :unprocessable_entity
  end
end

and in your final API endpoint that inherits from the Api::BaseController:

class Api::BeaconsController < Api::BaseController
  self.schema = BeaconSchema

  def create
    # Of course you can still have validations on a beacon level
    Beacon.create!(validation_result.to_h)
    head :no_content
  end
end

Summary

Dry-validation is one of my "must use" gems when working with any type of incoming data (even when it comes from my other systems). As presented above, it can be useful in many situations and can help reduce the number of responsibilities imposed on models.

This type of validation layer can also serve as a great starting point for any bigger refactoring process. Ensuring incoming data and schema consistency without having to refer to any business models allows to block the API without blocking the business and models behind it.

Cover photo by: brlnpics123 on Creative Commons 2.0 license. Changes made: added an overlay layer with an article title on top of the original picture.

ActiveResource relations – a bit of magic to make it look and feel more like ActiveModel relations

ActiveResource collection new problem

ActiveResource can be pretty helpful when you have a RESTful JSON API. Although it has some limitations. One of the most irritating is a lack of nested resources new scope method. When you have structure like this:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats
end

class DailyStat  < ActiveResource::Base
  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end

You can do some basic stuff:

User.last #=> User instance
User.all #=> [User, User]
user = User.last
user.daily_stats #=> [DailyStat, DailyStat]

But unfortunately if you try something like this:

user = User.last
stat = user.daily_stats.new
stat.save!

You'll get following error:

user.daily_stats.new
NoMethodError: undefined method `new' for #<ActiveResource::Collection:0x000000080d1a30>

Of course we can do this the other way around:

user = User.last
stats = DailyStat.new(user_id: user.id)
stats.save!

But it just doesn't seem right (well at least after working with ActiveRecord). ActiveResource::Collection doesn't support building resources through it.

alias_method and a bit of magic as a solution

To obtain such a behaviour we have to:

  • save original relation method using alias_method
  • create a module that will contain our extension that will allow us to build new resources directly
  • define relation method that will mix the module with original relation method output
  • return mixed relation output

Once we have all of this, we will be able to just:

user = User.last
new_stat = user.daily_stats.new
new_stat.save!

overwriting method without losing the original one

Ok, so we have our relation daily_stats method that will return us a given ActiveResource::Collection. We will have to overwrite it, but we can't lose the original one. To obtain this, we can use alias_method:

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :native_daily_stats, :daily_stats

  def daily_stats
    # Now we can do whatever we want here, because we can get
    # the ActiveResource::Collection of DailyStat via
    # native_daily_stats method
    # Something fancy will happen here...
    # and after that we will just return native_daily_stats
    native_daily_stats
  end
end

It's worth pointing out, that you can use this trick to redefine/change method without losing the original one. Especially when you can't use super (because it's not an inherited one, etc).

Extension module for our new daily_stats

Now we have to create our extension that will be used to modify the ActiveResource::Collection (but only in a daily_stats scope):

class DailyStat < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  # Here should go previously declarated DailyStat class code...
end

Hooking it all togethers

Finally, we can join all previously created elements in a new daily_stats method:

class User < ActiveResource::Base
  # User code...  

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

Now you can create resources that belong to other, directly via their scope.

TLl;DR version

class User < ActiveResource::Base
  self.site = 'your_api_end_point'

  has_many :daily_stats

  alias_method :original_daily_stats, :daily_stats

  def daily_stats
    original_daily_stats.extend DailyStat::RelationExtensions
    original_daily_stats
  end
end

class DailyStat  < ActiveResource::Base
  module RelationExtensions
    def new(params = {})
      # Here magic happens - original params contain relation details (user_id: user.id)
      params.merge!(original_params)
      resource_class.new(params)
    end
  end

  self.site = 'your_api_end_point'

  belongs_to :user

  schema do
    attribute 'videos_count', :integer
    attribute 'videos_excess', :integer
  end
end

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑