Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make changes for ActiveRecord #12

Merged
merged 1 commit into from
Jan 12, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ serialization.
require "attribute_helpers"

class Vehicle < ActiveRecord::Base
extend AttributeHelpers
prepend AttributeHelpers

attr_class :manufacturer
attr_symbol :status
Expand All @@ -51,7 +51,8 @@ car.status # :parked (the symbol) rather than "parked" (the string)

Note: while this gem was written to help with ActiveRecord
objects, it has **no dependencies** and works great with any database
backend (or none!). **You can extend it in pure Ruby classes just fine!**
backend (or none!). **You can prepend it into pure Ruby classes just
fine!**

## Contributing

Expand Down
1 change: 1 addition & 0 deletions attribute_helpers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.1"
spec.add_development_dependency "temping", "~> 3.2"
end
83 changes: 47 additions & 36 deletions lib/attribute_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,59 @@
# the database and your application code.

module AttributeHelpers
# Marks attributes as storing symbol values, providing setters and getters for
# the attributes that will allow the application to use them as a symbols but
# store them internally as strings.
# @param attrs [*Symbol] a list of the names of attributes that store symbols
def attr_symbol(*attrs)
attrs.each do |attr|
# Store the original methods for use in the overwritten ones.
original_getter = instance_method(attr)
original_setter = instance_method("#{attr}=")
# This module needs to be prepended to work in ActiveRecord classes. This is
# because ActiveRecord doesn't have accessors/mutators defined until an
# instance is created, which means we need to use the prepend + super()
# pattern because attempts to use instance_method + binding will fail since
# instance_method will not find the method in the class context as it will not
# exist until it is dynamically created when an instance is created. Prepend
# works for us because it inserts the behavior *below* the class in the
# inheritance hierarchy, so we can access the default ActiveRecord accessors/
# mutators through the use of super().
# More information here: http://stackoverflow.com/a/4471202/1103543
def self.prepended(klass)
# We need to store the module in a variable for use when we're in the class
# context.
me = self

# Overwrite the accessor.
define_method attr do
val = original_getter.bind(self).call
val && val.to_sym
end
# Marks attributes as storing symbol values, providing setters and getters
# for the attributes that will allow the application to use them as symbols
# but store them internally as strings.
# @param attrs [*Symbol] a list of the attributes that store symbols
klass.define_singleton_method :attr_symbol do |*attrs|
# Overwrite each attribute's methods.
attrs.each do |attr|
# Overwrite the accessor.
me.send(:define_method, attr) do
val = super()
val && val.to_sym
end

# Overwrite the mutator.
define_method "#{attr}=" do |val|
original_setter.bind(self).call(val && val.to_s)
# Overwrite the mutator.
me.send(:define_method, "#{attr}=") do |val|
super(val && val.to_s)
end
end
end
end

# Marks attributes as storing class values, providing setters and getters for
# the attributes that will allow the application to use them as a classes but
# store them internally as strings.
# @param attrs [*Symbol] a list of the names of attributes that store classes
def attr_class(*attrs)
attrs.each do |attr|
# Store the original methods for use in the overwritten ones.
original_getter = instance_method(attr)
original_setter = instance_method("#{attr}=")

# Overwrite the accessor.
define_method attr do
val = original_getter.bind(self).call
val && Kernel.const_get(val)
end
# Marks attributes as storing class values, providing setters and getters
# for the attributes that will allow the application to use them as classes
# but store them internally as strings.
# @param attrs [*Symbol] a list of the attributes that store classes
klass.define_singleton_method :attr_class do |*attrs|
# Overwrite each attribute's methods.
attrs.each do |attr|
# Overwrite the accessor.
# @raise [NameError] if the string can't be constantized
me.send(:define_method, attr) do
val = super()
val && Kernel.const_get(val)
end

# Overwrite the mutator.
define_method "#{attr}=" do |val|
original_setter.bind(self).call(val && val.to_s)
# Overwrite the mutator.
me.send(:define_method, "#{attr}=") do |val|
super(val && val.to_s)
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/attribute_helpers/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module AttributeHelpers
VERSION = "0.1.0"
VERSION = "0.1.1"
end
98 changes: 98 additions & 0 deletions spec/active_record_class_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
require "spec_helper"

RSpec.context "in an ActiveRecord class" do
Temping.create :active_record_test_class do
with_columns do |t|
t.string :symbol_attr
t.string :class_attr
end

prepend AttributeHelpers

attr_symbol :symbol_attr
attr_class :class_attr
end

let(:test_obj) { ActiveRecordTestClass.new }

describe ".attr_symbol" do
describe "getter" do
it "translates string to symbol" do
# Give it an intial value to be read.
test_obj[:symbol_attr] = "example"

expect(test_obj.symbol_attr).to eq :example
end

it "reads nil correctly" do
# Explicitly set the attribute to nil, though it's already nil by
# default.
test_obj[:symbol_attr] = nil

expect(test_obj.symbol_attr).to be_nil
end
end

describe "setter" do
it "translates symbol to string" do
test_obj.symbol_attr = :example
expect(test_obj[:symbol_attr]).to eq "example"
end

it "stores nil when nil is passed" do
# Give it an intial value, to make sure the nil set isn't just relying
# on default object behavior.
test_obj.symbol_attr = :example

test_obj.symbol_attr = nil
expect(test_obj[:symbol_attr]).to be_nil
end
end
end

describe ".attr_class" do
describe "getter" do
context "when string represents a real class" do
it "translates string to class" do
# Give it an intial value to be read.
test_obj[:class_attr] = "Object"

expect(test_obj.class_attr).to eq Object
end
end

context "when string does not represent a real class" do
it "raises an error" do
# Give it an intial value to be read.
test_obj[:class_attr] = "BogusClass"

expect { test_obj.class_attr }.to raise_error(NameError)
end
end

it "reads nil correctly" do
# Explicitly set the instance variable to nil, though it's already nil
# by default.
test_obj[:class_attr] = nil

expect(test_obj.class_attr).to be_nil
end
end

describe "setter" do
it "translates class to string" do
test_obj.class_attr = Object
expect(test_obj[:class_attr]).to eq "Object"
end

it "stores nil when nil is passed" do
# Give it an intial value, to make sure the nil set isn't just relying
# on default object behavior.
test_obj.class_attr = Object

test_obj.class_attr = nil
expect(test_obj[:class_attr]).to be_nil
end
end
end
end
45 changes: 24 additions & 21 deletions spec/attribute_helpers_spec.rb → spec/ruby_class_spec.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
require "spec_helper"

RSpec.describe AttributeHelpers do
describe ".attr_symbol" do
class AttributeHelperTestClass
extend AttributeHelpers
RSpec.context "in a pure Ruby class" do
class RubyTestClass
prepend AttributeHelpers

attr_accessor :symbol_attr
attr_accessor :symbol_attr
attr_accessor :class_attr

attr_symbol :symbol_attr
end
attr_symbol :symbol_attr
attr_class :class_attr
end

let(:test_obj) { AttributeHelperTestClass.new }
let(:test_obj) { RubyTestClass.new }

describe ".attr_symbol" do
describe "getter" do
it "translates string to symbol" do
# Give it an intial value to be read.
Expand Down Expand Up @@ -47,22 +49,23 @@ class AttributeHelperTestClass
end

describe ".attr_class" do
class AttributeHelperTestClass
extend AttributeHelpers

attr_accessor :class_attr

attr_class :class_attr
end
describe "getter" do
context "when string represents a real class" do
it "translates string to class" do
# Give it an intial value to be read.
test_obj.instance_variable_set(:@class_attr, "Object")

let(:test_obj) { AttributeHelperTestClass.new }
expect(test_obj.class_attr).to eq Object
end
end

describe "getter" do
it "translates string to class" do
# Give it an intial value to be read.
test_obj.instance_variable_set(:@class_attr, "Object")
context "when string does not represent a real class" do
it "raises an error" do
# Give it an intial value to be read.
test_obj.instance_variable_set(:@class_attr, "BogusClass")

expect(test_obj.class_attr).to eq Object
expect { test_obj.class_attr }.to raise_error(NameError)
end
end

it "reads nil correctly" do
Expand Down
5 changes: 5 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
require "codeclimate-test-reporter"
CodeClimate::TestReporter.start

# Connect to an in-memory database for ActiveRecord tests.
require "temping"
ActiveRecord::Base.
establish_connection(adapter: "sqlite3", database: ":memory:")

require "attribute_helpers"

RSpec.configure do |config|
Expand Down