Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added hash key factory.

The basic idea is a composite key. Each attribute defined for the hash
key is available as a virtual attribute. The adapter you use must know
how to natively store an id that is a hash or be able to convert it to
something native.
  • Loading branch information...
commit 5c0555ec3f6586a450a47925921bb90c9b033316 1 parent 750cdc2
@jnunemaker authored
View
1  lib/toy.rb
@@ -72,6 +72,7 @@ module Identity
autoload 'AbstractKeyFactory', 'toy/identity/abstract_key_factory'
autoload 'UUIDKeyFactory', 'toy/identity/uuid_key_factory'
autoload 'NativeUUIDKeyFactory', 'toy/identity/native_uuid_key_factory'
+ autoload 'HashKeyFactory', 'toy/identity/hash_key_factory'
end
end
View
4 lib/toy/identity.rb
@@ -7,11 +7,13 @@ module Identity
end
module ClassMethods
- def key(name_or_factory = :uuid)
+ def key(name_or_factory = :uuid, options = {})
@key_factory = if name_or_factory == :uuid
UUIDKeyFactory.new
elsif name_or_factory == :native_uuid
NativeUUIDKeyFactory.new
+ elsif name_or_factory == :hash
+ HashKeyFactory.new(options.merge(:model => self))
else
if name_or_factory.respond_to?(:next_key) && name_or_factory.respond_to?(:key_type)
name_or_factory
View
86 lib/toy/identity/hash_key_factory.rb
@@ -0,0 +1,86 @@
+module Toy
+ module Identity
+ class HashKeyFactory < AbstractKeyFactory
+ def initialize(args = {})
+ @model = args.fetch(:model)
+ @attributes = args.fetch(:attributes)
+ @to_key = args.fetch(:to_key) { default_to_key }
+
+ @accessors_module = Module.new
+ @model.send :include, @accessors_module
+
+ override_id_writer_to_typecast_values
+ define_virtual_attributes
+ override_virtual_writers_to_update_id
+ end
+
+ def key_type
+ Hash
+ end
+
+ # Public: Generates key for object. Uses virtual attributes that make of
+ # Hash key if they are available. If they aren't and their types have
+ # defaults, those will be used.
+ def next_key(object)
+ key = {}
+ @attributes.each do |name, type|
+ key[name] = object.send(name)
+ end
+ key
+ end
+
+ # Public: Generates url key for Rails based on object. Overrideable by
+ # providing your own to_key block. The default to_key block handles most
+ # of the types including uuid.
+ def to_key(object)
+ @to_key.call(object) if object.persisted?
+ end
+
+ # Private
+ def override_id_writer_to_typecast_values
+ @accessors_module.module_eval <<-EOM
+ def id=(value)
+ hash = {}
+ value.each do |key, value|
+ result = write_attribute(key, value)
+ hash[key] = result
+ end
+ super(hash)
+ end
+ EOM
+ end
+
+ # Private
+ def define_virtual_attributes
+ @attributes.each do |name, type|
+ @model.attribute name, type, :virtual => true
+ end
+ end
+
+ # Private
+ def override_virtual_writers_to_update_id
+ @attributes.each do |name, type|
+ @accessors_module.module_eval <<-EOM
+ def #{name}=(value)
+ id_will_change!
+ id[:#{name}] = write_attribute(:#{name}, value)
+ end
+ EOM
+ end
+ end
+
+ # Private: Ensures that uuid's convert to guid's when calling to_key.
+ def default_to_key
+ lambda { |object|
+ object.id.values.map { |value|
+ if value.respond_to?(:to_guid)
+ value.to_guid
+ else
+ value.to_s
+ end
+ }
+ }
+ end
+ end
+ end
+end
View
253 spec/toy/identity/hash_key_factory_spec.rb
@@ -0,0 +1,253 @@
+require 'helper'
+
+describe Toy::Identity::HashKeyFactory do
+ uses_objects('User')
+
+ let(:no_default_type) {
+ Class.new do
+ def self.to_store(value, *)
+ value
+ end
+
+ def self.from_store(value, *)
+ value
+ end
+ end
+ }
+
+ let(:bucket_type) {
+ Class.new do
+ def self.store_default
+ new('2012')
+ end
+
+ def self.to_store(value, *)
+ value
+ end
+
+ def self.from_store(value, *)
+ return value if value.is_a?(self)
+ new(value)
+ end
+
+ attr_reader :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def eql?(other)
+ self.class.eql?(other.class) && value == other.value
+ end
+
+ alias_method :==, :eql?
+ end
+ }
+
+ let(:uuid_type) {
+ Class.new(SimpleUUID::UUID) do
+ def self.store_default
+ SimpleUUID::UUID.new
+ end
+
+ def self.to_store(value, *)
+ return value if value.is_a?(SimpleUUID::UUID)
+ SimpleUUID::UUID.new(value)
+ end
+
+ def self.from_store(value, *)
+ return value if value.is_a?(SimpleUUID::UUID)
+ SimpleUUID::UUID.new(value)
+ end
+ end
+ }
+
+ let(:required_arguments) {
+ {model: User, attributes: {bucket: bucket_type, uuid: uuid_type}}
+ }
+
+ subject { described_class.new(required_arguments) }
+
+ before do
+ User.key subject
+ end
+
+ describe "#initialize" do
+ it "requires :model" do
+ args = required_arguments.reject { |key| key == :model }
+ expect {
+ described_class.new(args)
+ }.to raise_error(KeyError)
+ end
+
+ it "requires :attributes" do
+ args = required_arguments.reject { |key| key == :attributes }
+ expect {
+ described_class.new(args)
+ }.to raise_error(KeyError)
+ end
+
+ it "defines virtual attributes for each attribute" do
+ described_class.new(required_arguments)
+ required_arguments[:attributes].each_key do |name|
+ User.attributes.should have_key(name.to_s)
+ User.attributes[name.to_s].should be_virtual
+ end
+ end
+ end
+
+ describe "#next_key" do
+ context "with all types having store defaults" do
+ it "generates key based on defaults" do
+ bucket = bucket_type.new('2012')
+ uuid = uuid_type.new
+
+ bucket_type.should_receive(:store_default).and_return(bucket)
+ uuid_type.should_receive(:store_default).and_return(uuid)
+
+ key = subject.next_key(User.new)
+
+ key[:bucket].should eq(bucket)
+ key[:uuid].should eq(uuid)
+ end
+ end
+
+ context "when record has value for keys already" do
+ it "generates key based on set values" do
+ bucket = bucket_type.new('2012')
+ uuid = uuid_type.new
+ key = subject.next_key(User.new(bucket: bucket, uuid: uuid))
+
+ key[:bucket].should be(bucket)
+ key[:uuid].should be(uuid)
+ end
+ end
+
+ context "when record has type without default and no value for that key" do
+ subject {
+ described_class.new(required_arguments.merge({
+ attributes: {
+ bucket: no_default_type,
+ uuid: uuid_type,
+ }
+ }))
+ }
+
+ it "sets key to nil" do
+ uuid = uuid_type.new
+ key = subject.next_key(User.new(uuid: uuid))
+
+ key[:bucket].should be_nil
+ key[:uuid].should be(uuid)
+ end
+ end
+ end
+
+ describe "#eql?" do
+ it "returns true for same class and key type" do
+ subject.eql?(described_class.new(required_arguments)).should be_true
+ end
+
+ it "returns false for same class and different key type" do
+ other = described_class.new(required_arguments)
+ other.stub(:key_type).and_return(Integer)
+ subject.eql?(other).should be_false
+ end
+
+ it "returns false for different classes" do
+ subject.eql?(Object.new).should be_false
+ end
+ end
+
+ describe "#==" do
+ it "returns true for same class and key type" do
+ subject.==(described_class.new(required_arguments)).should be_true
+ end
+
+ it "returns false for same class and different key type" do
+ other = described_class.new(required_arguments)
+ other.stub(:key_type).and_return(Integer)
+ subject.==(other).should be_false
+ end
+
+ it "returns false for different classes" do
+ subject.==(Object.new).should be_false
+ end
+ end
+
+ describe "Declaring key to be hash" do
+ before do
+ User.key :hash, required_arguments.reject { |key| key == :model }
+ end
+
+ it "returns Hash as .key_type" do
+ User.key_type.should be(Hash)
+ end
+
+ it "sets id attribute to Hash type" do
+ User.attributes['id'].type.should be(Hash)
+ end
+
+ context "assigning one of the Hash's virtual attributes" do
+ before do
+ @user = User.new
+ @bucket = bucket_type.new('2011')
+ @user.bucket = @bucket
+ end
+
+ it "sets virtual attribute" do
+ @user.bucket.should be(@bucket)
+ end
+
+ it "marks that the virtual attribute has changed" do
+ @user.bucket_changed?.should be_true
+ end
+
+ it "updates id" do
+ @user.id[:bucket].should be(@bucket)
+ end
+
+ it "marks that id has changed" do
+ @user.id_changed?.should be_true
+ end
+ end
+
+ context "updating id" do
+ before do
+ @user = User.new
+ @bucket = bucket_type.new('2011')
+ @uuid = uuid_type.new
+ @user.id = {bucket: @bucket, uuid: @uuid}
+ end
+
+ it "updates id" do
+ @user.id.should eq({bucket: @bucket, uuid: @uuid})
+ end
+
+ it "updates virtual attributes" do
+ @user.bucket.should eq(@bucket)
+ @user.uuid.should eq(@uuid)
+ end
+ end
+
+ context "updating id with not type that gets typecast" do
+ before do
+ @user = User.new
+ @bucket = bucket_type.new('2011')
+ @uuid = uuid_type.new
+ @user.id = {bucket: '2011', uuid: @uuid}
+ end
+
+ it "correctly typecasts value in id" do
+ bucket = @user.id[:bucket]
+ bucket.should be_instance_of(bucket_type)
+ bucket.should eq(@bucket)
+ end
+
+ it "updates virtual attributes" do
+ @user.bucket.should eq(@bucket)
+ @user.uuid.should eq(@uuid)
+ end
+ end
+ end
+end
View
33 spec/toy/object_spec.rb
@@ -51,6 +51,39 @@
User.new.to_key.should be_nil
end
end
+
+ context "with hash" do
+ before do
+ User.key :hash, attributes: {location: String, name: String}
+ end
+
+ it "returns array with guid if persisted" do
+ user = User.new(location: 'IN', name: 'John')
+ user.stub(:persisted?).and_return(true)
+ user.to_key.should == ['IN', 'John']
+ end
+
+ it "returns nil if not persisted" do
+ User.new.to_key.should be_nil
+ end
+ end
+
+ context "with hash with uuid type" do
+ before do
+ User.key :hash, attributes: {bucket: String, track_id: SimpleUUID::UUID}
+ end
+
+ it "returns array with guid if persisted" do
+ id = SimpleUUID::UUID.new
+ user = User.new(bucket: '2012', track_id: id)
+ user.stub(:persisted?).and_return(true)
+ user.to_key.should == ['2012', id.to_guid]
+ end
+
+ it "returns nil if not persisted" do
+ User.new.to_key.should be_nil
+ end
+ end
end
describe "#to_param" do
Please sign in to comment.
Something went wrong with that request. Please try again.