Skip to content

Instantly share code, notes, and snippets.

@jdickey
Last active August 29, 2015 14:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdickey/c2823cf2053b1bd57e9f to your computer and use it in GitHub Desktop.
Save jdickey/c2823cf2053b1bd57e9f to your computer and use it in GitHub Desktop.
My latest WTF exploration with Ruby, Rails and metaprogramming.

It was the best of times; it was the worst of times...

I can relate. Figuring out odd code is like that; usually all at the same time.

Code I Expect To Work, Does

Consider the first class listed below, when run in a Rails console for my current test-bed app. When I run a Rails-console pry session as...

$ bundle exec rails c
Loading development environment (Rails 4.2.0)

Frame number: 0/5
[1] pry(main)> ActiveRecord::Base.establish_connection
=> #<ActiveRecord::ConnectionAdapters::ConnectionPool:0x007f9d96a83e38
# ... elided; obviously successful...
[2] pry(main)> require_relative 'first_class.rb'
=> true
[3] pry(main)> foo = Foo.new name: 'User Name', email: 'user@example.com', profile: 'The Profile'
=> #<Foo:0x007f9d969833a8 @email="user@example.com", @name="User Name", @profile="The Profile">
[4] pry(main)> foo.valid?
=> true
[5] pry(main)> foo.instance_variable_set :@name, '  Bad  Name '
=> "  Bad  Name "
[6] pry(main)> foo.valid? == false
=> true
[7] pry(main)> foo.errors.full_messages == ["Name may not have leading whitespace", "Name may not have trailing whitespace", "Name may not have adjacent whitespace"]
=> true
[8] exit
$

...everything works as I expect (as evidenced by the comparisons yielding => true).

Code I Expect To Work, Does Not

However, when I attempt to take what I've gleaned from the above and have a second try, things go pear-shaped:

$ bundle exec rails c
Loading development environment (Rails 4.2.0)

Frame number: 0/5
[1] pry(main)> ActiveRecord::Base.establish_connection
=> #<ActiveRecord::ConnectionAdapters::ConnectionPool:0x007fdc165c6fa8
# ... elided; obviously successful...
[2] pry(main)> require_relative 'second_try.rb'
=> true
[3] pry(main)> bar = $bar
=> #<Object:0x007faf8167a628>
[4] pry(main)> bar.instance_variable_set :@name, 'User Name'
=> "User Name"
[5] pry(main)> bar.instance_variable_set :@email, 'user@example.com'
=> "user@example.com"
[6] pry(main)> bar.instance_variable_set :@profile, 'User Profile'
=> "User Profile"
[7] pry(main)> bar.valid? == true
=> true
[8] pry(main)> bar.instance_variable_set :@name, '  Bad  Name '
=> "  Bad  Name "
[9] pry(main)> bar.valid? == false
=> true

So far, so good. Here's the problem:

[10] pry(main)> bar.errors.full_messages == ["Name may not have leading whitespace", "Name may not have trailing whitespace", "Name may not have adjacent whitespace"]
NoMethodError: undefined method `human_attribute_name' for Object:Class
from /Users/jeffdickey/src/rails/meldd/new_poc/vendor/ruby/2.2.0/gems/activemodel-4.2.0/lib/active_model/errors.rb:380:in `full_message'
[11] pry(main)> exit
$

I could understand ActiveModel bitching at me because I hadn't defined a method somewhere in my bolting-on-to-a-simple-Object-instance except that it had no qualms about an (apparently) equivalently-constructed Class instance.

What am I missing?

(For your convenience, the code in the app/entites/user/internals/name_validator.rb file included by both source files is now included in this Gist for easy reference.)

require 'active_model/validations'
require_relative 'app/entities/user/internals/name_validator'
class Foo
include Comparable
extend Forwardable
include ActiveModel::Validations
attr_reader :email, :name, :profile
validates :name, presence: true, length: { minimum: 6 }
validate :validate_name
validates_email_format_of :email
def self.model_name
ActiveModel::Name.new self, nil, 'User'
end
def initialize(attrs)
@name = attrs[:name]
@email = attrs[:email]
@profile = attrs[:profile]
end
private
def validate_name
Entity::User::Internals::NameValidator.new(name).validate.add_errors_to_model(self)
end
end
require 'active_model/validations'
require_relative 'app/entities/user/internals/name_validator'
bar = Object.new
bar.instance_eval { extend Forwardable }
bar.class_eval do
include Comparable
include ActiveModel::Validations
end
bar.class_eval do
attr_reader :email, :name, :profile
validates :name, presence: true, length: { minimum: 6 }
validate :validate_name
validates_email_format_of :email
private
def validate_name
Entity::User::Internals::NameValidator.new(name).validate.add_errors_to_model(self)
end
end
bar.class.define_singleton_method :model_name do
ActiveModel::Name.new self, nil, 'User'
end
$bar = bar
# Namespace containing all application-defined entities.
module Entity
# The `User` class is the *core business-logic entity* modelling users in the
# system. The core class encapsulates logic not specific to one use case or
# group of use cases (such as authorisation). It also establishes a namespace
# which encapsulates more specific entity-oriented responsibilities.
class User
# Internal classes used exclusively by User class.
module Internals
# Validates user name string following rules documented at #validate.
class NameValidator
attr_reader :errors
# Initialise a new instance by setting the `name` attribute to the
# specified value.
# @param name [String] User name to be validated by this class instance.
def initialize(name)
@name = name
@errors = []
end
# Validates user name by three separate, related rules.
# A user name MUST NOT have:
#
# 1. leading or trailing whitespace characters;
# 2. whitespace characters other than the space character
# 3. consecutive whitespace characters within its text ("Bad Value")
#
# Calls methods to perform each validation step and add an appropriate
# message to an internal list if that validation step fails.
def validate
check_for_spaces_at_ends
check_for_invalid_whitespace
check_for_adjacent_whitespace
self
end
# Adds error messages generated on behalf of #validate to an ActiveModel
# instance passed in as a parameter.
# @param Model instance quacking like ActiveModel::Validations.
def add_errors_to_model(model)
@errors.each { |message| model.errors.add :name, message }
self
end
private
attr_reader :name
# Adds an error message to the internal list if the `name` attribute has
# either leading or trailing whitespace as specified by the parameter.
# @param strip_where [Symbol] Either `:leading`, to check for spaces at
# the beginning of the `name` attribute,
# OR `:trailing` to check at the end.
def add_error_if_whitespace(strip_where)
strips = {
leading: :lstrip,
trailing: :rstrip
}
error_message = format 'may not have %s whitespace',
strip_where.to_s
@errors << error_message if name != name.send(strips[strip_where])
self
end
# Adds an error message to the internal list if the `name` attribute
# contains two or more consecutive internal whitespace characters.
def check_for_adjacent_whitespace
return if name.to_s.strip == name.to_s.strip.gsub(/\s{2,}/, '?')
@errors << 'may not have adjacent whitespace'
self
end
# Adds an error message to the internal list if the `name` attribute
# contains whitespace *other than* the space character. (`' '`)
def check_for_invalid_whitespace
expected = name.to_s.strip.gsub(/ {2,}/, ' ')
return if expected == expected.gsub(/\s/, ' ')
@errors << 'may not have whitespace other than spaces'
self
end
# Calls the internal #add_error_if_whitespace method to check first for
# leading, then for trailing, whitespace at the ends of the `name`
# attributes.
def check_for_spaces_at_ends
return if name.to_s == name.to_s.strip
add_error_if_whitespace :leading
add_error_if_whitespace :trailing
self
end
end # class Entity::User::Internals::NameValidator
end # module Entity::User::Internals
end # class Entity::User
end # module Entity
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment