This week I worked on a new web interface for our open source project License Finder.
The license_finder
gem persists dependency information out to a YAML file; however, we wanted to persist these same
dependency objects to a SQL database for the website.
Step 1: Persistence base class
In order to accomplish this, I had to perform a series of refactorings that would make it possible to swap out YAML persistence
with an ActiveRecord
persistence layer. The LicenseFinder::Dependency
class had never been setup make persistence injectable,
so the first step was moving all persistence-related functionality out into a seperate base class, and giving it an ActiveRecord-style API:
module LicenseFinder
module Persistence
class Dependency
class Database
#... YAML 'database' implementation details
end
attr_accessor *LicenseFinder::DEPENDENCY_ATTRIBUTES
class << self
def find_by_name(name)
attributes = database.find { |a| a['name'] == name }
new(attributes) if attributes
end
def delete_all
database.delete_all
end
def all
database.all.map { |attributes| new(attributes) }
end
def unapproved
all.select {|d| d.approved == false }
end
def update(attributes)
database.update attributes
end
def destroy_by_name(name)
database.destroy_by_name name
end
private
def database
@database ||= Database.new
end
end
def initialize(attributes = {})
update_attributes_without_saving attributes
end
def config
LicenseFinder.config
end
def update_attributes new_values
update_attributes_without_saving(new_values)
save
end
def approved?
!!approved
end
def save
self.class.update(attributes)
end
def destroy
self.class.destroy_by_name(name)
end
def attributes
attributes = {}
LicenseFinder::DEPENDENCY_ATTRIBUTES.each do |attrib|
attributes[attrib] = send attrib
end
attributes
end
private
def update_attributes_without_saving(new_values)
new_values.each do |key, value|
send("#{key}=", value)
end
end
end
end
end
With all of the persistence-related functionality in this base class, I could now update the LicenseFinder::Dependency
class to inherit from this:
module LicenseFinder
class Dependency < LicenseFinder::Persistence::Dependency
#...
end
end
I also created a shared example for describing how persistence should work (regardless of the underlying persistence implementation):
shared_examples_for "a persistable dependency" do
let(:klass) { described_class }
let(:attributes) do
{
'name' => "spec_name",
'version' => "2.1.3",
'license' => "GPLv2",
'approved' => false,
'notes' => 'some notes',
'homepage' => 'homepage',
'license_files' => ['/Users/pivotal/foo/lic1', '/Users/pivotal/bar/lic2'],
'readme_files' => ['/Users/pivotal/foo/Readme1', '/Users/pivotal/bar/Readme2'],
'source' => "bundle",
'bundler_groups' => ["test"]
}
end
before do
klass.delete_all
end
describe '.new' do
subject { klass.new(attributes) }
context "with known attributes" do
it "should set the all of the attributes on the instance" do
attributes.each do |key, value|
if key != "approved"
subject.send("#{key}").should equal(value), "expected #{value.inspect} for #{key}, got #{subject.send("#{key}").inspect}"
else
subject.approved?.should == value
end
end
end
end
context "with unknown attributes" do
before do
attributes['foo'] = 'bar'
end
it "should raise an exception" do
expect { subject }.to raise_exception(NoMethodError)
end
end
end
describe '.unapproved' do
it "should return all unapproved dependencies" do
klass.new(name: "unapproved dependency", approved: false).save
klass.new(name: "approved dependency", approved: true).save
unapproved = klass.unapproved
unapproved.count.should == 1
unapproved.collect(&:approved?).any?.should be_false
end
end
describe '.find_by_name' do
subject { klass.find_by_name gem_name }
let(:gem_name) { "foo" }
context "when a gem with the provided name exists" do
before do
klass.new(
'name' => gem_name,
'version' => '0.0.1'
).save
end
its(:name) { should == gem_name }
its(:version) { should == '0.0.1' }
end
context "when no gem with the provided name exists" do
it { should == nil }
end
end
describe "#config" do
it 'should respond to it' do
klass.new.should respond_to(:config)
end
end
describe '#attributes' do
it "should return a hash containing the values of all the accessible properties" do
dep = klass.new(attributes)
attributes = dep.attributes
LicenseFinder::DEPENDENCY_ATTRIBUTES.each do |name|
attributes[name].should == dep.send(name)
end
end
end
describe '#save' do
it "should persist all of the dependency's attributes" do
dep = klass.new(attributes)
dep.save
saved_dep = klass.find_by_name(dep.name)
attributes.each do |key, value|
if key != "approved"
saved_dep.send("#{key}").should eql(value), "expected #{value.inspect} for #{key}, got #{saved_dep.send("#{key}").inspect}"
else
saved_dep.approved?.should == value
end
end
end
end
describe "#update_attributes" do
it "should update the provided attributes with the provided values" do
gem = klass.new(attributes)
updated_attributes = {"version" => "new_version", "license" => "updated_license"}
gem.update_attributes(updated_attributes)
saved_gem = klass.find_by_name(gem.name)
saved_gem.version.should == "new_version"
saved_gem.license.should == "updated_license"
end
end
describe "#destroy" do
it "should remove itself from the database" do
foo_dep = klass.new(name: "foo")
bar_dep = klass.new(name: "bar")
foo_dep.save
bar_dep.save
expect { foo_dep.destroy }.to change { klass.all.count }.by -1
klass.all.count.should == 1
klass.all.first.name.should == "bar"
end
end
end
Step 2 – Make persistence autoloadable
Next, I wanted to make persistence autoloadable in the gem (so that other persistence solutions could simply create their ownLicenseFinder::Persistence::Dependency
implementation before doing a require "license_finder"
:
module LicenseFinder
module Persistence
autoload :Dependency, 'license_finder/persistence/yaml/dependency'
autoload :Configuration, 'license_finder/persistence/yaml/configuration'
end
end
Step 3 – Create new persistence implementation
Now, creating an ActiveRecord persistence implementation was as simple as:
module LicenseFinder
module Persistence
class Dependency < ActiveRecord::Base
serialize :license_files
serialize :readme_files
serialize :bundler_groups
serialize :children
serialize :parents
belongs_to :config
scope :unapproved, where(approved: false)
end
end
end
require "license_finder"
And the test for this persistence implementation:
require "spec_helper"
require_relative "path/to/LicenseFinder/spec/support/shared_examples/persistence/dependency.rb"
describe LicenseFinder::Persistence::Dependency do
it_behaves_like "a persistable dependency"
end
About the Author
![]()