ruby-on-railsrubycallbackrails-geocodergeocoder

How to validate that the distance_to of two Geocoder objects is less than 200 mts within a Rails before_save callback?


I am a RoR beginner and I need to check that the distance between user and gym Geocoder objects is less than 200 meters in order to create a check_in object in the DB

Here is my code:

app/models/check_in.rb

class CheckIn < ApplicationRecord
  belongs_to :user
  belongs_to :gym

  before_save :check_in_distance

  protected

  def check_in_distance
    gym = Gym.find_by(id: params[:gym_id])
    distance_to_gym = gym.distance_to([43.9,-98.6])
    if distance_to_gym < 200
      return true
    end
  end

end


app/controllers/api/v1/check_inscontroller.rb


class Api::V1::CheckInsController < ApplicationController

  before_action :authenticate_request!

  def check_in
    @check_in = CheckIn.create!(check_in_params.merge( user: current_user))
    render json: CheckInBlueprint.render(@check_in, root: :data)
  end

  private

  def check_in_params
    params.require(:check_in).permit(:gym_id, :check_in_latitude,
                                   :check_in_longitude)
  end
end

Solution

  • You want a custom validation and not a callback:

    class CheckIn < ApplicationRecord
      validate :distantance_to_gym
    
      def distantance_to_gym
        distance = gym.distance_to([check_in_latitude, check_in_longitude])
        errors.add(:base, 'check in is too far from gym') if distance < 200
      end
    end
    

    Don't use create! in your controller. Sure its great for lazy debugging, but it raises an exception if the user passes invalid input. Invalid input is not an exceptional event and exceptions should not be used for normal control flow.

    The "bang" methods such as .create! should only be used in non-interactive contexts like seed files where creating the record should not be expected to fail or when wrapping it in a transaction to cause a rollback.

    Use the normal "non-bang" methods such as save and create and check if the record was actually persisted and respond accordingly:

    # see https://github.com/rubocop/ruby-style-guide#namespace-definition
    module Api
      module V1
        class CheckInsController < ApplicationController
          # this should be moved to the parent controller
          # use a secure by default model where you opt out instead
          before_action :authenticate_request!
     
          def check_in
            @check_in = CheckIn.new(check_in_params) do |c| 
              c.user = current_user
            end
            if @check_in.save
              render json: CheckInBlueprint.render(@check_in, root: :data),
                     status: :created
            else
              render json: @check_in.errors.full_messages,
                     status: :unprocessable_entity
            end
         end
    
         private
    
         def check_in_params
           params.require(:check_in).permit(:gym_id, :check_in_latitude,
                                       :check_in_longitude)
         end
        end
      end
    end