Created
December 11, 2012 04:00
-
-
Save snusnu/4255792 to your computer and use it in GitHub Desktop.
minimal aliasing strategies for dm2
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
require 'equalizer' | |
require 'abstract_type' | |
module DataMapper | |
module Relation | |
class Graph | |
class Node | |
class Aliases | |
include Enumerable | |
include Equalizer.new(:index) | |
attr_reader :index | |
attr_reader :header | |
protected :index | |
def initialize(index, aliases = {}) | |
@index = index | |
@aliases = aliases | |
@header = @index.header | |
end | |
def each(&block) | |
return to_enum unless block_given? | |
@aliases.each(&block) | |
self | |
end | |
def join(other, join_definition) | |
joined_index = index.join(other.index, join_definition) | |
self.class.new(joined_index, index.aliases(joined_index)) | |
end | |
def rename(aliases) | |
self.class.new(index.rename(aliases), aliases) | |
end | |
class Index | |
include Equalizer.new(:entries) | |
attr_reader :entries | |
attr_reader :header | |
def initialize(entries, strategy) | |
@entries = entries | |
@inverted = @entries.invert | |
@header = @entries.values.to_set | |
@strategy = strategy.new(self) | |
end | |
def join(*args) | |
@strategy.join(*args) | |
end | |
def rename(aliases) | |
self.class.new(renamed_entries(aliases), @strategy.class) | |
end | |
def aliases(other) | |
entries.each_with_object({}) { |(key, name), aliases| | |
other_name = other[key] | |
aliases[name] = other_name if name != other_name | |
} | |
end | |
def renamed_join_key_entries(join_definition) | |
entries.each_with_object({}) { |(key, name), renamed| | |
join_definition.each do |left_key, right_key| | |
renamed[key] = right_key if name == left_key | |
end | |
} | |
end | |
def renamed_clashing_entries(other, join_definition) | |
entries.each_with_object({}) { |(key, name), renamed| | |
next if !other.include?(name) || join_definition.key?(name) | |
renamed[key] = key | |
} | |
end | |
def [](key) | |
entries[key] | |
end | |
def include?(name) | |
entries.value?(name) | |
end | |
private | |
def renamed_entries(aliases) | |
aliases.each_with_object(entries.dup) { |(from, to), renamed| | |
renamed[@inverted.fetch(from)] = to | |
} | |
end | |
end # class Index | |
class Strategy | |
include AbstractType | |
def initialize(index) | |
@index = index | |
end | |
def join(index, join_definition) | |
index.class.new(index_entries(index, join_definition), self.class) | |
end | |
abstract_method :index_entries | |
private :index_entries | |
private | |
attr_reader :index | |
def join_key_entries(*args) | |
index.renamed_join_key_entries(*args) | |
end | |
def clashing_entries(*args) | |
index.renamed_clashing_entries(*args) | |
end | |
class InnerJoin < self | |
private | |
def index_entries(other_index, join_definition) | |
index.entries.dup. | |
update(clashing_entries(other_index, join_definition)). | |
update(other_index.entries) | |
end | |
end | |
class NaturalJoin < self | |
private | |
def index_entries(other_index, join_definition) | |
index.entries.dup. | |
update(join_key_entries(join_definition)). | |
update(clashing_entries(other_index, join_definition)). | |
update(other_index.entries) | |
end | |
end # class NaturalJoin | |
end # class Strategy | |
end # class Aliases | |
end # class Node | |
end # class Graph | |
end # module Relation | |
end # module DataMapper | |
require 'rspec' | |
describe DataMapper::Relation::Graph::Node::Aliases, '#rename' do | |
subject { object.rename(aliases) } | |
let(:object) { described_class.new(songs_index) } | |
let(:songs_index) { described_class::Index.new(songs_entries, strategy) } | |
let(:strategy) { described_class::Strategy::NaturalJoin } | |
let(:songs_entries) {{ | |
:songs_id => :id, | |
:songs_title => :title, | |
}} | |
let(:aliases) {{ | |
:id => :foo_id, | |
:title => :foo_title | |
}} | |
let(:expected_index) { described_class::Index.new(expected_entries, strategy) } | |
let(:expected_entries) {{ | |
:songs_id => :foo_id, | |
:songs_title => :foo_title, | |
}} | |
it { should be_instance_of(object.class) } | |
its(:index) { should eql(expected_index) } | |
end | |
shared_examples_for 'a command method' do | |
it 'returns self' do | |
should equal(object) | |
end | |
end | |
shared_examples_for 'an #each method' do | |
it_should_behave_like 'a command method' | |
context 'with no block' do | |
subject { object.each } | |
it { should be_instance_of(to_enum.class) } | |
it 'yields the expected values' do | |
subject.to_a.should eql(object.to_a) | |
end | |
end | |
end | |
describe DataMapper::Relation::Graph::Node::Aliases, '#each' do | |
subject { object.each { |field, aliased_field| yields[field] = aliased_field } } | |
let(:yields) { {} } | |
before do | |
object.should be_instance_of(described_class) | |
end | |
context 'with a block' do | |
context "using Aliases::Strategy::NaturalJoin" do | |
let(:strategy) { described_class::Strategy::NaturalJoin } | |
context "when no join has been performed" do | |
let(:object) { described_class.new(songs_index) } | |
let(:songs_index) { described_class::Index.new(songs_entries, strategy) } | |
let(:songs_entries) {{ | |
:songs_id => :id, | |
:songs_title => :title, | |
}} | |
it_should_behave_like 'an #each method' | |
it 'yields correct aliases' do | |
expect { subject }.to_not change { yields.dup } | |
end | |
end | |
context "when a join has been performed" do | |
let(:object) { songs.join(song_tags, join_definition) } | |
let(:songs) { described_class.new(songs_index) } | |
let(:song_tags) { described_class.new(song_tags_index) } | |
let(:songs_index) { described_class::Index.new(songs_entries, strategy) } | |
let(:song_tags_index) { described_class::Index.new(song_tags_entries, strategy) } | |
let(:join_definition) {{ | |
:id => :song_id | |
}} | |
context "with unique attribute names across both relations" do | |
let(:songs_entries) {{ | |
:songs_id => :id, | |
:songs_title => :title, | |
}} | |
let(:song_tags_entries) {{ | |
:song_tags_song_id => :song_id, | |
:song_tags_tag_id => :tag_id, | |
}} | |
it_should_behave_like 'an #each method' | |
it 'yields correct aliases' do | |
expect { subject }.to change { yields.dup }. | |
from({}). | |
to(:id => :song_id) | |
end | |
end | |
context "and the left join key has been renamed before already" do | |
let(:object) { songs_X_song_tags.join(song_comments, other_join_definition) } | |
let(:other_join_definition) {{ | |
:song_id => :song_id | |
}} | |
let(:songs_X_song_tags) { songs.join(song_tags, join_definition) } | |
let(:song_comments) { described_class.new(song_comments_index) } | |
let(:song_comments_index) { described_class::Index.new(song_comments_entries, strategy) } | |
let(:songs_entries) {{ | |
:songs_id => :id, | |
:songs_title => :title, | |
}} | |
let(:song_tags_entries) {{ | |
:song_tags_song_id => :song_id, | |
:song_tags_tag_id => :tag_id, | |
}} | |
let(:song_comments_entries) {{ | |
:song_comments_song_id => :song_id, | |
:song_comments_comment_id => :comment_id, | |
}} | |
it_should_behave_like 'an #each method' | |
it 'yields correct aliases' do | |
expect { subject }.to_not change { yields.dup } | |
end | |
end | |
context "with clashing attribute names" do | |
context "only before renaming join keys" do | |
let(:songs_entries) {{ | |
:songs_id => :id, | |
:songs_title => :title, | |
}} | |
let(:song_tags_entries) {{ | |
:song_tags_id => :id, | |
:song_tags_song_id => :song_id, | |
:song_tags_tag_id => :tag_id, | |
}} | |
it_should_behave_like 'an #each method' | |
it 'yields correct aliases' do | |
expect { subject }.to change { yields.dup }. | |
from({}). | |
to(:id => :song_id) | |
end | |
end | |
context "before and after renaming join keys" do | |
context "and the clashing attribute is not part of the join keys" do | |
let(:songs_entries) {{ | |
:songs_id => :id, | |
:songs_title => :title, | |
:songs_created_at => :created_at | |
}} | |
let(:song_tags_entries) {{ | |
:song_tags_song_id => :song_id, | |
:song_tags_tag_id => :tag_id, | |
:song_tags_created_at => :created_at, | |
}} | |
it_should_behave_like 'an #each method' | |
it 'yields correct aliases' do | |
expect { subject }.to change { yields.dup }. | |
from({}). | |
to( | |
:id => :song_id, | |
:created_at => :songs_created_at | |
) | |
end | |
end | |
context "and the clashing attribute matches a join key" do | |
let(:songs_entries) {{ | |
:songs_id => :id, | |
:songs_title => :title, | |
:songs_song_id => :song_id, | |
}} | |
let(:song_tags_entries) {{ | |
:song_tags_id => :id, | |
:song_tags_song_id => :song_id, | |
:song_tags_tag_id => :tag_id, | |
}} | |
it_should_behave_like 'an #each method' | |
it 'yields correct aliases' do | |
expect { subject }.to change { yields.dup }. | |
from({}). | |
to( | |
:id => :song_id, | |
:song_id => :songs_song_id | |
) | |
end | |
end | |
end | |
end | |
end | |
end | |
end | |
end | |
describe DataMapper::Relation::Graph::Node::Aliases do | |
subject { object.new(index) } | |
let(:object) { described_class } | |
let(:index) { mock('index', :header => mock) } | |
before do | |
subject.should be_instance_of(object) | |
end | |
it { should be_kind_of(Enumerable) } | |
it 'case matches Enumerable' do | |
(Enumerable === subject).should be(true) | |
end | |
end |
@snusnu I think you can remove Index#update_name
and Index#keys
and replace them with:
def initialize(entries, strategy)
@entries = entries
@inverted = entries.invert
@strategy = strategy.new(self)
end
# ...
def renamed_entries(aliases)
aliases.each_with_object(@entries.dup) do |(from, to), renamed_entries|
renamed_entries[@inverted.fetch(from)] = to
end
end
@snusnu here's my current best attempt at refactoring/simplification: https://gist.github.com/4256126
@dkubb i've updated the gist to match your latest code. i also use attr_reader
's now when feasible.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@snusnu can Index#keys be written as:
It's not exactly elegant, but it creates less temporary objects than the original.