Created
September 24, 2011 20:44
-
-
Save sycobuny/1239836 to your computer and use it in GitHub Desktop.
We're asserting the hell out of some arguments
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Sequel | |
class Model | |
STRIP_UNSAFE_VAR_NAMES = /[^a-z_]/i | |
######### | |
protected | |
######### | |
# | |
# Asserts that arguments match a certain class, and raises errors if | |
# they do not. The arguments are passed as return values from a block | |
# which makes certain assumptions for how the variables would be named | |
# in the scope of the method. For instance, an "account" variable should | |
# be an Account object. | |
# | |
# The simplest way to use the method is to pass a block which returns a | |
# single Symbol (containing the variable name) or Class (the class of | |
# which the variable should be a type). The earlier stated assumptions | |
# hold in this case. You can also pass an array (which can mix Symbol | |
# and Class elements) which makes the same assumptions about each | |
# individual element. | |
# | |
# The more explicit mechanism of declaring variables using a hash should | |
# be reserved for when variables aren't named according to their type. | |
# The hash should be formatted with keys being the variable names, and | |
# values being the classes. | |
# | |
# Only the "returning a single Symbol" method of checking types requires | |
# that the argument be a Symbol, all other methods can take a String, | |
# Class, Symbol, or whatever other representation can be converted to | |
# a string that describes the name and/or type. | |
# | |
# This method will only validate arguments if $config.logging is set to | |
# :debug, as it's designed to help in writing code. By the time it's | |
# running in a production environment, the kinks requiring this code | |
# should be addressed. | |
# | |
# @private | |
# @param [Proc] block Block returning argument types | |
# @raise [ArgumentError] If arguments don't match type | |
# | |
# @example Using a single Symbol class to define an argument | |
# def drop(account) | |
# assert { :account } # verify account is an Account object | |
# account.delete # perform an Account-specific action | |
# end | |
# | |
# @example Verifying multiple arguments in an Array | |
# def get_privileges(account, channel) | |
# assert { [:account, Channel] } # mixed type is allowed | |
# Privilege[:account => account, :channel => channel].all | |
# end | |
# | |
# @example Validating arguments that don't use the naming convention | |
# def message(from, to, message) | |
# # strings are allowed as keys or values | |
# assert { {'from' => Account, :to => 'Account'} } | |
# Message.new(from, to, message).send! | |
# end | |
# | |
def self.assert(&block) | |
return unless $config.logging == :debug | |
# get the arguments and the scope in which they were declared. | |
args = block.call | |
bind = block.binding | |
# predeclare to_check, which contains our list of variables to | |
# validate, and errors, which contains the list of ones that don't | |
# meet their respective criterion. | |
to_check = {} | |
errors = [] | |
# turn :some_arg into SomeArg. | |
to_class = lambda do |str| | |
camelize(str.to_s.gsub(STRIP_UNSAFE_VAR_NAMES, '')) | |
end | |
# turn SomeClass into some_class. | |
to_var = lambda do |cls| | |
underscore(cls.to_s.gsub(STRIP_UNSAFE_VAR_NAMES, '')) | |
end | |
# if they just passed :some_arg or SomeClass, camelize or | |
# de-camelize as necessary. | |
if args.is_a? Symbol or args.is_a? Class | |
to_check[ to_var.call(args) ] = to_class.call(args) | |
# if they passed an array of :some_arg or SomeClass, camelize or | |
# de-camelize each arg, as necessary. | |
elsif args.is_a? Array | |
args.each do |arg| | |
to_check[ to_var.call(arg) ] = to_class.call(arg) | |
end | |
# they were pretty explicit here, :some_arg => SomeClass. we still | |
# wipe their args clean but we know there's probably something named | |
# differently here. | |
elsif args.is_a? Hash | |
args.each do |varname, classname| | |
to_check[ to_var.call(varname) ] = to_class.call(classname) | |
end | |
end | |
# check each variable in its original scope. | |
to_check.each do |var, klass| | |
unless bind.eval("#{var}.kind_of?(#{klass})") | |
errors << "argument #{var} must be of type #{klass}" | |
end | |
end | |
# all arguments passed muster, yay! | |
return if errors.empty? | |
# this is just me being an English pedant. | |
if errors.length == 1 | |
errstr = errors[0] | |
else | |
errstr = ary[0 ... -1].join(', ') | |
errstr = [str, ary[-1]].join(' and ') | |
errstr = str[0].upcase + str[1 .. -1] + '.' | |
end | |
# raise an ArgumentError in the calling scope, so we're a ghost in | |
# the process and don't send people scurrying to this file to find | |
# a problem that's probably somewhere else. | |
raise ArgumentError, errstr, caller | |
end | |
# | |
# Instance-level wrapper for Sequel::Model.assert, see the documentation | |
# for that method. | |
# | |
# @private | |
# @param [Proc] block Block returning argument types | |
# @raise [ArgumentError] if arguments don't match type | |
# | |
def assert(&block); self.class.assert(&block) end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Verify that an argument is a valid descendent of a particular class. You can | |
# optionally provide a name for the argument to make the error message that | |
# results if the check fails a little bit more clear. | |
# | |
# This method will only validate arguments if $config.logging is set to | |
# :debug, as it's designed to help in writing code. By the time it's | |
# running in a production environment, the kinks requiring this code | |
# should be addressed. | |
# | |
# @param [Object] var The variable to check | |
# @param [Class] klass The class of which "var" should be a descendent | |
# @param [String, #to_s] name The optional name of the variable | |
# @raise [ArgumentError] If the "var" is not a "klass" object | |
# | |
# @example Minimal arguments to verify that "account" is an Account object | |
# assert(account, Database::Account) | |
# | |
# @example More descriptive, say what variable name we're testing | |
# assert(chan, Database::ChannelService::Channel, 'chan') | |
# | |
def assert(var, klass, name = nil) | |
return if $config.logging == :debug | |
unless var.kind_of? klass | |
if name | |
errstr = "#{name} must be of type #{klass}" | |
else | |
errstr = "#{var} must be of type #{klass}" | |
end | |
raise ArgumentError, errstr | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment