You need Ensurance

Been plowing through a massive refactoring of a legacy Rails app.   One of the things I find myself doing all the time now is ensuring.  It’s a handy little concern that works like this:

In the old days, we’d do this:

class SomeClass
  def some_method(person_id)
    person = Person.find(person_id)
 
    # then proceed to do something useful
  end
end

But when testing in the console I was always having to lookup a Person record to get the id, or sometimes I’d remember the ID. Wouldn’t it be nice if I could pass anything(within reason) and be able to ensure that I have a Person record?

[granted…this is Rails specific]

module Ensurable
  extend ActiveSupport::Concern
 
  ## ---
  # Allow you to ensure you have the class you expect... it's similar to
  # result = value.is_a?(Person) ? value : Person.find(value)
  #
  # You can add fields to "ensure_by" (:id is included always)
  # e.g.
  #  if you add `ensure_by :token` to the User class
  #   User.ensure() works
  #   User.ensure(:user_id) works
  #   User.ensure(:token) works
  #
  #  .ensure() returns nil if the record is not found
  #  .ensure!() throws an exception if the record is not found
 
  class_methods do
    def ensure_by(*args)
      @_ensure_by ||= [:id]
      @_ensure_by += [args].flatten
    end
 
    def ensure(thing = nil)
      return nil unless thing.present?
      return thing if thing.is_a?(self)
      found = nil
 
      @_ensure_by ||= [:id]
      @_ensure_by.each do |ensure_field|
        value = thing.try(:fetch, ensure_field.to_sym, nil) || thing.try(:fetch, ensure_field.to_s, nil) || thing
        found = self.find_by(ensure_field => value)
        break if found.is_a?(self)
      end
 
      found
    end
 
    def ensure!(thing = nil)
      result = self.ensure(thing)
      raise ActiveRecord::RecordNotFound unless result
      result
    end
 
  end
end

Now, you can just:

class SomeClass
  def some_method(person)
    person = Person.ensure!(person)
 
    # then proceed to do something useful
  end
end

Once you start using it, you’ll wish all objects would be as friendly.

Example:

class Date
  def self.ensure(thing)
    thing.is_a?(Date) ? thing : Date.parse(thing.to_s)
  end
end

enjoy. 🙂

Update: June 10, 2017

I’ve found these two extensions to Time and Date most helpful:

class Time
  def self.ensure(thing)
    case thing
    when Time
      thing
    when Date
      thing.beginning_of_day
    when Integer, Float
      Time.at(thing)
    when String
      if thing.to_i.to_s == thing
        Time.at(thing.to_i)
      elsif thing.to_f.to_s == thing
        Time.at(thing.to_f)
      else
        Time.parse(thing)
      end
    else
      raise ArgumentError.new("Unknown Type for Time to ensure: #{thing.class.name}")
    end
  end
end
 
class Date
  def self.ensure(thing)
    case thing
    when Date
      thing
    when String
      Date.parse(thing)
    else
      if thing.respond_to?(:to_date)
        thing.to_date
      else       
        raise ArgumentError.new("Unknown Type for Date to ensure: #{thing.class.name}")
      end
    end
  end
end

Leave a Reply

Your email address will not be published. Required fields are marked *