by Dean
Ruby on Rails allows for plugins to extend its functionality. My first attempt at one is nothing special but I am going to blog about it anyway
I needed a way to check if a number is within a range before saving a record to the database. Rails has a number of good validations built in, but not one that does that. In two or three hours, I was able to piece together a simple plugin that adds “validates_range_of” to my AR models. The code is a little ugly until I can get Greg to install a syntax highlighting plugin for this blog.
-
module ActiveRecord
-
module Validations
-
module ClassMethods
-
def validates_range_of(*attrs)
-
options = { :on => :save }
-
options.update(attrs.pop) if attrs.last.is_a?(Hash)
-
attrs.flatten!
-
unless options[:message]
-
if options[:maximum] && options[:minimum]
-
options[:message] = ‘must be betweeen ‘ + options[:minimum].to_s + ‘ and ‘ + options[:maximum].to_s
-
elsif options[:maximum]
-
options[:message] = ‘must be less than or equal to ‘ + options[:maximum].to_s
-
elsif options[:minimum]
-
options[:message] = ‘must be greater than or equal to ‘ + options[:minimum].to_s
-
else
-
raise ArgumentError, ‘Range unspecified. Specify the :maximum and/or :minimum.’
-
end
-
end
-
-
validates_numericality_of( attrs, options )
-
-
validates_each(attrs, options) do |record, attr_name, value|
-
if ((!options[:maximum].nil?) && (!options[:minimum].nil?)) &&
-
(value > options[:maximum] || value < options[:minimum])
-
record.errors.add( attr_name, options[:message] )
-
elsif (!options[:maximum].nil?) && value > options[:maximum]
-
record.errors.add( attr_name, options[:message] )
-
elsif (!options[:minimum].nil?) && value < options[:minimum]
-
record.errors.add( attr_name, options[:message] )
-
end
-
end
-
end
-
end
-
end
-
end
As you can see this plugin extends AR::Validations. So my models can use validates_range_of to check minimum and maximum values. Let’s look at the BJCP Scoresheet I needed it for:
-
class BjcpScoresheet < ActiveRecord::Base
-
belongs_to :brew_session
-
validates_associated :brew_session
-
validates_range_of :brew_session_id, { :minimum => 1, :message => ‘does not belong to a brew session’ }
-
validates_range_of :aroma_score, { :minimum => 1, :maximum => 12, :only_integer => true }
-
validates_range_of :appearance_score, { :minimum => 1, :maximum => 3, :only_integer => true }
-
validates_range_of :flavor_score, { :minimum => 1, :maximum => 20, :only_integer => true }
-
validates_range_of :mouthfeel_score, { :minimum => 1, :maximum => 10, :only_integer => true }
-
validates_range_of :impression_score, { :minimum => 1, :maximum => 10, :only_integer => true }
How nice - reusable user input validation.
–Dean
January 23, 2007 by Dean
Before the death of my drives I had a flexible Measurement class written up. As most of you know brewing involves all kinds of measurement - hop weights, boil volume, bitterness, etc. The class served as a base that more specific classes would inherit and provided the framework for converting between different measurement systems. For example, the SpecificGravity class would inherit Measurement and provide the conversion code to switch between SG and Plato.
The database would save the scalar and units and instantiate an aggregation of these two pieces. When the units changed, back into the database it went with the new values. It was very easy to work with, but took me quite a bit of time to write.
“Why not use one of the libraries already available?” you might ask. The short answer is that they do not fit my needs. Firstly, none of them produced objects that I could stuff in the database as a aggregation - most are intended as great extensions to various number classes. Secondly, and most importantly, none of them dealt with Bitterness, Color or Specific Gravity, which is really why I need a measurement class. Lastly, most of them only handled linear transformations:
m2 = a * m1 + b
To convert from SG to Plato you need to use a cubic polynomial, ugh.
So I wrote my own, then re-wrote it, and it was beautiful. Now I am re-engineering the whole thing. In addition, I’ll have to figure out where I got the Plato to SG reverse conversion. I remember trying to solve the cubic equation, then finding something that actually worked.
It is a little faster-going because I wrote it all before, but I had some tr1cky 31337 code in there that I will have to figure out again.
Always make sure your backups are working and current.
–Dean