From 540cd28f4075b5bd8d221a144b97e78433712f3c Mon Sep 17 00:00:00 2001 From: Damian Janowski & Michel Martens Date: Fri, 25 Jun 2010 23:59:27 -0500 Subject: [PATCH] Get rid of Ohm::Collection and use Ohm::Key. --- lib/ohm.rb | 151 +++--- lib/ohm/collection.rb | 186 -------- lib/ohm/key.rb | 14 +- test/model_module_test.rb | 951 -------------------------------------- test/model_test.rb | 157 ++++--- 5 files changed, 158 insertions(+), 1301 deletions(-) delete mode 100644 lib/ohm/collection.rb delete mode 100644 test/model_module_test.rb diff --git a/lib/ohm.rb b/lib/ohm.rb index 286f7e1..be5a6b5 100644 --- a/lib/ohm.rb +++ b/lib/ohm.rb @@ -6,7 +6,6 @@ require File.join(File.dirname(__FILE__), "ohm", "validations") require File.join(File.dirname(__FILE__), "ohm", "compat-1.8.6") require File.join(File.dirname(__FILE__), "ohm", "key") -require File.join(File.dirname(__FILE__), "ohm", "collection") module Ohm @@ -93,24 +92,19 @@ def inspect class Collection include Enumerable + attr :key attr :ids attr :model def initialize(key, model, db = nil) + @key = key @model = model.unwrap - @ids = self.class::Raw.new(key, db || @model.db) + @db = db || @model.db + @ids = Key.new(@key, @db) end - def <<(model) - ids << model.id - end - - alias add << - - def each(&block) - ids.each do |id| - block.call(model[id]) - end + def add(model) + self << model end def key @@ -153,58 +147,30 @@ def sort_by(att, options = {}) end end - def delete(model) - ids.delete(model.id) - model - end - def clear - ids.clear + ids.del end def concat(models) - ids.concat(models.map { |model| model.id }) + models.each { |model| add(model) } self end def replace(models) - ids.replace(models.map { |model| model.id }) - self - end - - def include?(model) - ids.include?(model.id) + clear + concat(models) end def empty? - ids.empty? + !ids.exists end - def size - ids.size + def to_a + all end - - def all - ids.to_a.map(&model) - end - - alias to_a all end - class Set - include Enumerable - - attr :key - attr :ids - attr :model - - def initialize(key, model, db = nil) - @key = key - @model = model.unwrap - @db = db || @model.db - @ids = Key.new(@key, @db) - end - + class Set < Collection def each(&block) ids.smembers.each { |id| block.call(model[id]) } end @@ -223,14 +189,6 @@ def size ids.scard end - def clear - ids.del - end - - def empty? - ! ids.exists - end - def delete(member) ids.srem(member.id) end @@ -260,7 +218,7 @@ def keys(hash) def find(options) source = keys(options) - target = Key.new(key.volatile + Key[*source], model.db) + target = source.inject(key.volatile) { |chain, key| chain + key } target.sinterstore(ids, *source) Set.new(target, Wrapper.wrap(model)) end @@ -271,7 +229,7 @@ def except(options) keys << model.index_key_for(k, v) end - target = Key.new(key.volatile - Key[*keys], model.db) + target = keys.inject(key.volatile) { |chain, key| chain + key } target.sdiffstore(ids, *keys) Set.new(target, Wrapper.wrap(model)) end @@ -316,6 +274,14 @@ def first(options = {}) sort(options).first end end + + def include?(model) + ids.sismember(model.id) + end + + def inspect + "#" + end end class Index < Set @@ -345,7 +311,7 @@ def find(options) if source.size == 1 Set.new(source.first, Wrapper.wrap(model)) else - target = Key.new(Key[*source].volatile, model.db) + target = source.inject(key.volatile) { |chain, key| chain + key } target.sinterstore(*source) Set.new(target, Wrapper.wrap(model)) end @@ -353,26 +319,49 @@ def find(options) end class List < Collection - Raw = Ohm::List + def each(&block) + ids.lrange(0, -1).each { |id| block.call(model[id]) } + end - def shift - if id = ids.shift - model[id] - end + def <<(model) + ids.rpush(model.id) + end + + alias push << + + def first + id = ids.lindex(0) + model[id] if id end def pop - if id = ids.pop - model[id] - end + id = ids.rpop + model[id] if id + end + + def shift + id = ids.lpop + model[id] if id end def unshift(model) - ids.unshift(model.id) + ids.lpush(model.id) + end + + def all + ids.lrange(0, -1).map(&model) + end + + def size + ids.llen + end + + def include?(model) + ids.lrange(0, -1).include?(model.id) end def inspect - "#" + "#" end end @@ -453,8 +442,8 @@ def self.counter(name) # is created. # # @param name [Symbol] Name of the list. - def self.list(name, model = nil) - attr_collection_reader(name, :List, model) + def self.list(name, model) + attr_collection_reader(name, List, model) collections << name unless collections.include?(name) end @@ -463,8 +452,8 @@ def self.list(name, model = nil) # operations like union, join, and membership checks are important. # # @param name [Symbol] Name of the set. - def self.set(name, model = nil) - attr_collection_reader(name, :Set, model) + def self.set(name, model) + attr_collection_reader(name, Set, model) collections << name unless collections.include?(name) end @@ -591,12 +580,8 @@ def self.to_reference end def self.attr_collection_reader(name, type, model) - if model - model = Wrapper.wrap(model) - define_memoized_method(name) { Ohm::Model::const_get(type).new(key[name], model, db) } - else - define_memoized_method(name) { Ohm::const_get(type).new(key[name], db) } - end + model = Wrapper.wrap(model) + define_memoized_method(name) { type.new(key[name], model, db) } end def self.define_memoized_method(name, &block) @@ -721,7 +706,7 @@ def decr(att, count = 1) end # Export the id and errors of the object. The `to_hash` takes the opposite - # approach of providing all the attributes and instead favors a + # approach of providing all the attributes and instead favors a # white listed approach. # # @example @@ -740,10 +725,10 @@ def decr(att, count = 1) # # => true # # # for cases where you want to provide white listed attributes just do: - # + # # class Person < Ohm::Model # def to_hash - # super.merge(:name => name) + # super.merge(:name => name) # end # end # @@ -759,7 +744,7 @@ def to_hash end def to_json(*args) - to_hash.to_json(*args) + to_hash.to_json(*args) end def attributes diff --git a/lib/ohm/collection.rb b/lib/ohm/collection.rb deleted file mode 100644 index 5130d1e..0000000 --- a/lib/ohm/collection.rb +++ /dev/null @@ -1,186 +0,0 @@ -module Ohm - class Collection - include Enumerable - - attr_accessor :key, :db - - def initialize(key, db = Ohm.redis) - self.key = key - self.db = db - end - - def each(&block) - all.each(&block) - end - - # Return the values as model instances, ordered by the options supplied. - # Check redis documentation to see what values you can provide to each option. - # - # @param options [Hash] options to sort the collection. - # @option options [#to_s] :by Model attribute to sort the instances by. - # @option options [#to_s] :order (ASC) Sorting order, which can be ASC or DESC. - # @option options [Integer] :limit (all) Number of items to return. - # @option options [Integer] :start (0) An offset from where the limit will be applied. - # - # @example Get the first ten users sorted alphabetically by name: - # - # @event.attendees.sort(:by => :name, :order => "ALPHA", :limit => 10) - # - # @example Get five posts sorted by number of votes and starting from the number 5 (zero based): - # - # @blog.posts.sort(:by => :votes, :start => 5, :limit => 10") - def sort(options = {}) - return [] if empty? - options[:start] ||= 0 - options[:limit] = [options[:start], options[:limit]] if options[:limit] - db.sort(key, options) - end - - # Sort the model instances by id and return the first instance - # found. If a :by option is provided with a valid attribute name, the - # method sort_by is used instead and the option provided is passed as the - # first parameter. - # - # @see #sort - # @return [Ohm::Model, nil] Returns the first instance found or nil. - def first(options = {}) - options = options.merge(:limit => 1) - sort(options).first - end - - def [](index) - first(:start => index) - end - - def to_ary - all - end - - def ==(other) - to_ary == other - end - - # @return [true, false] Returns whether or not the collection is empty. - def empty? - size.zero? - end - - # Clears the values in the collection. - def clear - db.del(key) - self - end - - # Appends the given values to the collection. - def concat(values) - values.each { |value| self << value } - self - end - - # Replaces the collection with the passed values. - def replace(values) - clear - concat(values) - end - end - - # Represents a Redis list. - # - # @example Use a list attribute. - # - # class Event < Ohm::Model - # attribute :name - # list :participants - # end - # - # event = Event.create :name => "Redis Meeting" - # event.participants << "Albert" - # event.participants << "Benoit" - # event.participants.all - # # => ["Albert", "Benoit"] - class List < Collection - - # @param value [#to_s] Pushes value to the tail of the list. - def << value - db.rpush(key, value) - end - - alias push << - - # @return [String] Return and remove the last element of the list. - def pop - db.rpop(key) - end - - # @return [String] Return and remove the first element of the list. - def shift - db.lpop(key) - end - - # @param value [#to_s] Pushes value to the head of the list. - def unshift(value) - db.lpush(key, value) - end - - # @return [Array] Elements of the list. - def all - db.lrange(key, 0, -1) - end - - # @return [Integer] Returns the number of elements in the list. - def size - db.llen(key) - end - - def include?(value) - all.include?(value) - end - - def inspect - "#" - end - end - - # Represents a Redis set. - # - # @example Use a set attribute. - # - # class Company < Ohm::Model - # attribute :name - # set :employees - # end - # - # company = Company.create :name => "Redis Co." - # company.employees << "Albert" - # company.employees << "Benoit" - # company.employees.all #=> ["Albert", "Benoit"] - # company.employees.include?("Albert") #=> true - class Set < Collection - - # @param value [#to_s] Adds value to the list. - def << value - db.sadd(key, value) - end - - def delete(value) - db.srem(key, value) - end - - def include?(value) - db.sismember(key, value) - end - - def all - db.smembers(key) - end - - # @return [Integer] Returns the number of elements in the set. - def size - db.scard(key) - end - - def inspect - "#" - end - end -end diff --git a/lib/ohm/key.rb b/lib/ohm/key.rb index 350234d..aa79f47 100644 --- a/lib/ohm/key.rb +++ b/lib/ohm/key.rb @@ -9,26 +9,20 @@ def initialize(name, redis = nil) super(name.to_s) end - Volatile = new("~") - - def self.[](*args) - new(args.join(":")) - end - def [](key) - self.class[self, key] + self.class.new("#{self}:#{key}", @redis) end def volatile - self.index(Volatile) == 0 ? self : Volatile[self] + self.index("~") == 0 ? self : self.class.new("~", @redis)[self] end def +(other) - self.class.new("#{self}+#{other}") + self.class.new("#{self}+#{other}", @redis) end def -(other) - self.class.new("#{self}-#{other}") + self.class.new("#{self}-#{other}", @redis) end [:append, :blpop, :brpop, :decr, :decrby, :del, :exists, :expire, diff --git a/test/model_module_test.rb b/test/model_module_test.rb deleted file mode 100644 index aa9f6ce..0000000 --- a/test/model_module_test.rb +++ /dev/null @@ -1,951 +0,0 @@ -# encoding: UTF-8 - -require File.join(File.dirname(__FILE__), "test_helper") -require "ostruct" - -module Model - class Post < Ohm::Model - attribute :body - list :comments - list :related, Post - end - - class User < Ohm::Model - attribute :email - set :posts, Post - end - - class Person < Ohm::Model - attribute :name - index :initial - - def validate - assert_present :name - end - - def initial - name[0, 1].upcase - end - end - - class Event < Ohm::Model - attribute :name - counter :votes - set :attendees, Person - - attribute :slug - - def write - self.slug = name.to_s.downcase - super - end - end -end - -class ScopedModelsTest < Test::Unit::TestCase - setup do - Ohm.flush - end - - context "An event initialized with a hash of attributes" do - should "assign the passed attributes" do - event = Model::Event.new(:name => "Ruby Tuesday") - assert_equal event.name, "Ruby Tuesday" - end - end - - context "An event created from a hash of attributes" do - should "assign an id and save the object" do - event1 = Model::Event.create(:name => "Ruby Tuesday") - event2 = Model::Event.create(:name => "Ruby Meetup") - - assert_equal "1", event1.id - assert_equal "2", event2.id - end - - should "return the unsaved object if validation fails" do - assert Model::Person.create(:name => nil).kind_of?(Model::Person) - end - end - - context "An event updated from a hash of attributes" do - class ::Model::Meetup < Ohm::Model - attribute :name - attribute :location - - def validate - assert_present :name - end - end - - should "assign an id and save the object" do - event = Model::Meetup.create(:name => "Ruby Tuesday") - event.update(:name => "Ruby Meetup") - assert_equal "Ruby Meetup", event.name - end - - should "return false if the validation fails" do - event = Model::Meetup.create(:name => "Ruby Tuesday") - assert !event.update(:name => nil) - end - - should "save the attributes in UTF8" do - event = Model::Meetup.create(:name => "32° Kisei-sen") - assert_equal "32° Kisei-sen", Model::Meetup[event.id].name - end - - should "delete the attribute if set to nil" do - event = Model::Meetup.create(:name => "Ruby Tuesday", :location => "Los Angeles") - assert_equal "Los Angeles", Model::Meetup[event.id].location - assert event.update(:location => nil) - assert_equal nil, Model::Meetup[event.id].location - end - - should "delete the attribute if set to an empty string" do - event = Model::Meetup.create(:name => "Ruby Tuesday", :location => "Los Angeles") - assert_equal "Los Angeles", Model::Meetup[event.id].location - assert event.update(:location => "") - assert_equal nil, Model::Meetup[event.id].location - end - end - - context "Model definition" do - should "not raise if an attribute is redefined" do - assert_nothing_raised do - class ::Model::RedefinedModel < Ohm::Model - attribute :name - attribute :name - end - end - end - - should "not raise if a counter is redefined" do - assert_nothing_raised do - class ::Model::RedefinedModel < Ohm::Model - counter :age - counter :age - end - end - end - - should "not raise if a list is redefined" do - assert_nothing_raised do - class ::Model::RedefinedModel < Ohm::Model - list :todo - list :todo - end - end - end - - should "not raise if a set is redefined" do - assert_nothing_raised do - class ::Model::RedefinedModel < Ohm::Model - set :friends - set :friends - end - end - end - - should "not raise if a collection is redefined" do - assert_nothing_raised do - class ::Model::RedefinedModel < Ohm::Model - list :toys - set :toys - end - end - end - - should "not raise if a index is redefined" do - assert_nothing_raised do - class ::Model::RedefinedModel < Ohm::Model - attribute :color - index :color - index :color - end - end - end - end - - context "Finding an event" do - setup do - Ohm.redis.sadd("Model::Event:all", 1) - Ohm.redis.hset("Model::Event:1", "name", "Concert") - end - - should "return an instance of Event" do - assert Model::Event[1].kind_of?(Model::Event) - assert_equal 1, Model::Event[1].id - assert_equal "Concert", Model::Event[1].name - end - end - - context "Finding a user" do - setup do - Ohm.redis.sadd("Model::User:all", 1) - Ohm.redis.hset("Model::User:1", "email", "albert@example.com") - end - - should "return an instance of User" do - assert Model::User[1].kind_of?(Model::User) - assert_equal 1, Model::User[1].id - assert_equal "albert@example.com", Model::User[1].email - end - - should "allow to map ids to models" do - assert_equal [Model::User[1]], [1].map(&Model::User) - end - end - - context "Updating a user" do - setup do - Ohm.redis.sadd("Model::User:all", 1) - Ohm.redis.set("Model::User:1:email", "albert@example.com") - - @user = Model::User[1] - end - - should "change its attributes" do - @user.email = "maria@example.com" - assert_equal "maria@example.com", @user.email - end - - should "save the new values" do - @user.email = "maria@example.com" - @user.save - - @user.email = "maria@example.com" - @user.save - - assert_equal "maria@example.com", Model::User[1].email - end - end - - context "Creating a new model" do - should "assign a new id to the event" do - event1 = Model::Event.new - event1.create - - event2 = Model::Event.new - event2.create - - assert !event1.new? - assert !event2.new? - - assert_equal "1", event1.id - assert_equal "2", event2.id - end - end - - context "Saving a model" do - should "create the model if it is new" do - event = Model::Event.new(:name => "Foo").save - assert_equal "Foo", Model::Event[event.id].name - end - - should "save it only if it was previously created" do - event = Model::Event.new - event.name = "Lorem ipsum" - event.create - - event.name = "Lorem" - event.save - - assert_equal "Lorem", Model::Event[event.id].name - end - - should "allow to hook into write" do - event = Model::Event.create(:name => "Foo") - - assert_equal "foo", event.slug - end - end - - context "Delete" do - should "delete an existing model" do - class ::Model::ModelToBeDeleted < Ohm::Model - attribute :name - set :foos - list :bars - end - - @model = Model::ModelToBeDeleted.create(:name => "Lorem") - - @model.foos << "foo" - @model.bars << "bar" - - id = @model.id - - @model.delete - - assert_nil Ohm.redis.get(Model::ModelToBeDeleted.key[id]) - assert_nil Ohm.redis.get(Model::ModelToBeDeleted.key[id][:name]) - assert_equal Array.new, Ohm.redis.smembers(Model::ModelToBeDeleted.key[id][:foos]) - assert_equal Array.new, Ohm.redis.lrange(Model::ModelToBeDeleted.key[id][:bars], 0, -1) - - assert Model::ModelToBeDeleted.all.empty? - end - - should "be no leftover keys" do - class ::Model::Foo < Ohm::Model - attribute :name - index :name - end - - assert_equal [], Ohm.redis.keys("*") - - Model::Foo.create(:name => "Bar") - - assert_equal ["Model::Foo:1", "Model::Foo:1:_indices", "Model::Foo:all", "Model::Foo:id", "Model::Foo:name:QmFy"], Ohm.redis.keys("*").sort - - Model::Foo[1].delete - - assert_equal ["Model::Foo:id"], Ohm.redis.keys("*") - end - end - - context "Listing" do - should "find all" do - event1 = Model::Event.new - event1.name = "Ruby Meetup" - event1.create - - event2 = Model::Event.new - event2.name = "Ruby Tuesday" - event2.create - - all = Model::Event.all - - assert all.detect {|e| e.name == "Ruby Meetup" } - assert all.detect {|e| e.name == "Ruby Tuesday" } - end - end - - context "Sorting" do - should "sort all" do - Model::Person.create :name => "D" - Model::Person.create :name => "C" - Model::Person.create :name => "B" - Model::Person.create :name => "A" - - assert_equal %w[A B C D], Model::Person.all.sort_by(:name, :order => "ALPHA").map { |person| person.name } - end - - should "return an empty array if there are no elements to sort" do - assert_equal [], Model::Person.all.sort_by(:name) - end - - should "return the first element sorted by id when using first" do - Model::Person.create :name => "A" - Model::Person.create :name => "B" - assert_equal "A", Model::Person.all.first.name - end - - should "return the first element sorted by name if first receives a sorting option" do - Model::Person.create :name => "B" - Model::Person.create :name => "A" - assert_equal "A", Model::Person.all.first(:by => :name, :order => "ALPHA").name - end - - should "return attribute values when the get parameter is specified" do - Model::Person.create :name => "B" - Model::Person.create :name => "A" - - assert_equal "A", Model::Person.all.sort_by(:name, :get => :name, :order => "ALPHA").first - end - end - - context "Loading attributes" do - setup do - event = Model::Event.new - event.name = "Ruby Tuesday" - @id = event.create.id - end - - should "load attributes lazily" do - event = Model::Event[@id] - - assert_nil event.send(:instance_variable_get, "@name") - assert_equal "Ruby Tuesday", event.name - end - - should "load attributes as a strings" do - event = Model::Event.create(:name => 1) - - assert_equal "1", Model::Event[event.id].name - end - end - - context "Attributes of type Set" do - setup do - @person1 = Model::Person.create(:name => "Albert") - @person2 = Model::Person.create(:name => "Bertrand") - @person3 = Model::Person.create(:name => "Charles") - - @event = Model::Event.new - @event.name = "Ruby Tuesday" - end - - should "not be available if the model is new" do - assert_raise Ohm::Model::MissingID do - @event.attendees << Model::Person.new - end - end - - should "remove an element if sent :delete" do - @event.create - @event.attendees << @person1 - @event.attendees << @person2 - @event.attendees << @person3 - assert_equal ["1", "2", "3"], @event.attendees.ids.sort - @event.attendees.delete(@person2) - assert_equal ["1", "3"], Model::Event[@event.id].attendees.ids.sort - end - - should "return true if the set includes some member" do - @event.create - @event.attendees << @person1 - @event.attendees << @person2 - assert @event.attendees.include?(@person2) - assert !@event.attendees.include?(@person3) - end - - should "return instances of the passed model" do - @event.create - @event.attendees << @person1 - - assert_equal [@person1], @event.attendees.all - assert_equal @person1, @event.attendees[@person1.id] - end - - should "return the size of the set" do - @event.create - @event.attendees << @person1 - @event.attendees << @person2 - @event.attendees << @person3 - assert_equal 3, @event.attendees.size - end - - should "empty the set" do - @event.create - @event.attendees << @person1 - - @event.attendees.clear - - assert @event.attendees.empty? - end - - should "replace the values in the set" do - @event.create - @event.attendees << @person1 - - assert_equal [@person1], @event.attendees.all - - @event.attendees.replace([@person2, @person3]) - - assert_equal [@person2, @person3], @event.attendees.sort - end - - should "filter elements" do - @event.create - @event.attendees.add(@person1) - @event.attendees.add(@person2) - - assert_equal [@person1], @event.attendees.find(:initial => "A").all - assert_equal [@person2], @event.attendees.find(:initial => "B").all - assert_equal [], @event.attendees.find(:initial => "Z").all - end - end - - context "Attributes of type List" do - setup do - @post = Model::Post.new - @post.body = "Hello world!" - @post.create - end - - should "return an array" do - assert @post.comments.all.kind_of?(Array) - end - - should "append elements with push" do - @post.comments.push "1" - @post.comments << "2" - - assert_equal ["1", "2"], @post.comments.all - end - - should "keep the inserting order" do - @post.comments << "1" - @post.comments << "2" - @post.comments << "3" - assert_equal ["1", "2", "3"], @post.comments.all - end - - should "keep the inserting order after saving" do - @post.comments << "1" - @post.comments << "2" - @post.comments << "3" - @post.save - assert_equal ["1", "2", "3"], Model::Post[@post.id].comments.all - end - - should "respond to each" do - @post.comments << "1" - @post.comments << "2" - @post.comments << "3" - - i = 1 - @post.comments.each do |c| - assert_equal i, c.to_i - i += 1 - end - end - - should "return the size of the list" do - @post.comments << "1" - @post.comments << "2" - @post.comments << "3" - assert_equal 3, @post.comments.size - end - - should "return the last element with pop" do - @post.comments << "1" - @post.comments << "2" - assert_equal "2", @post.comments.pop - assert_equal "1", @post.comments.pop - assert @post.comments.empty? - end - - should "return the first element with shift" do - @post.comments << "1" - @post.comments << "2" - assert_equal "1", @post.comments.shift - assert_equal "2", @post.comments.shift - assert @post.comments.empty? - end - - should "push to the head of the list with unshift" do - @post.comments.unshift "1" - @post.comments.unshift "2" - assert_equal "1", @post.comments.pop - assert_equal "2", @post.comments.pop - assert @post.comments.empty? - end - - should "empty the list" do - @post.comments.unshift "1" - @post.comments.clear - - assert @post.comments.empty? - end - - should "replace the values in the list" do - @post.comments.replace(["1", "2"]) - - assert_equal ["1", "2"], @post.comments - end - - should "add models" do - @post.related.add(Model::Post.create(:body => "Hello")) - - assert_equal ["2"], @post.related.ids - end - - should "find elements in the list" do - another_post = Model::Post.create - - @post.related.add(another_post) - - assert @post.related.include?(another_post) - assert !@post.related.include?(Model::Post.create) - end - - should "unshift models" do - @post.related.unshift(Model::Post.create(:body => "Hello")) - @post.related.unshift(Model::Post.create(:body => "Goodbye")) - - assert_equal ["3", "2"], @post.related.ids - - assert_equal "3", @post.related.shift.id - - assert_equal "2", @post.related.pop.id - - assert_nil @post.related.pop - end - end - - context "Applying arbitrary transformations" do - require "date" - - class MyActiveRecordModel - def self.find(id) - return new if id.to_i == 1 - end - - def id - 1 - end - - def ==(other) - id == other.id - end - end - - class ::Model::Appointment < Ohm::Model - end - - class ::Model::Calendar < Ohm::Model - list :holidays, lambda { |v| Date.parse(v) } - list :subscribers, lambda { |id| MyActiveRecordModel.find(id) } - list :appointments, ::Model::Appointment - end - - class ::Model::Appointment - attribute :text - reference :subscriber, lambda { |id| MyActiveRecordModel.find(id) } - end - - setup do - @calendar = Model::Calendar.create - - @calendar.holidays.ids << "2009-05-25" - @calendar.holidays.ids << "2009-07-09" - - @calendar.subscribers << MyActiveRecordModel.find(1) - end - - should "apply a transformation" do - assert_equal [Date.new(2009, 5, 25), Date.new(2009, 7, 9)], @calendar.holidays.all - - assert_equal ["1"], @calendar.subscribers.ids.all - assert_equal [MyActiveRecordModel.find(1)], @calendar.subscribers.all - end - - should "allow lambdas in references" do - appointment = Model::Appointment.create(:subscriber => MyActiveRecordModel.find(1)) - assert_equal MyActiveRecordModel.find(1), appointment.subscriber - end - - should "work with models too" do - @calendar.appointments.add(Model::Appointment.create(:text => "Meet with Bertrand")) - - assert_equal [Model::Appointment[1]], Model::Calendar[1].appointments.sort - end - end - - context "Sorting lists and sets" do - setup do - @post = Model::Post.create(:body => "Lorem") - @post.comments << 2 - @post.comments << 3 - @post.comments << 1 - end - - should "sort values" do - assert_equal %w{1 2 3}, @post.comments.sort - end - end - - context "Sorting lists and sets by model attributes" do - setup do - @event = Model::Event.create(:name => "Ruby Tuesday") - @event.attendees << Model::Person.create(:name => "D") - @event.attendees << Model::Person.create(:name => "C") - @event.attendees << Model::Person.create(:name => "B") - @event.attendees << Model::Person.create(:name => "A") - end - - should "sort the model instances by the values provided" do - people = @event.attendees.sort_by(:name, :order => "ALPHA") - assert_equal %w[A B C D], people.map { |person| person.name } - end - - should "accept a number in the limit parameter" do - people = @event.attendees.sort_by(:name, :limit => 2, :order => "ALPHA") - assert_equal %w[A B], people.map { |person| person.name } - end - - should "use the start parameter as an offset if the limit is provided" do - people = @event.attendees.sort_by(:name, :limit => 2, :start => 1, :order => "ALPHA") - assert_equal %w[B C], people.map { |person| person.name } - end - end - - context "Collections initialized with a Model parameter" do - setup do - @user = Model::User.create(:email => "albert@example.com") - @user.posts.add Model::Post.create(:body => "D") - @user.posts.add Model::Post.create(:body => "C") - @user.posts.add Model::Post.create(:body => "B") - @user.posts.add Model::Post.create(:body => "A") - end - - should "return instances of the passed model" do - assert_equal Model::Post, @user.posts.first.class - end - end - - context "Counters" do - setup do - @event = Model::Event.create(:name => "Ruby Tuesday") - end - - should "raise ArgumentError if the attribute is not a counter" do - assert_raise ArgumentError do - @event.incr(:name) - end - end - - should "be zero if not initialized" do - assert_equal 0, @event.votes - end - - should "be able to increment a counter" do - @event.incr(:votes) - assert_equal 1, @event.votes - end - - should "be able to decrement a counter" do - @event.decr(:votes) - assert_equal -1, @event.votes - end - end - - context "Comparison" do - setup do - @user = Model::User.create(:email => "foo") - end - - should "be comparable to other instances" do - assert_equal @user, Model::User[@user.id] - - assert_not_equal @user, Model::User.create - assert_not_equal Model::User.new, Model::User.new - end - - should "not be comparable to instances of other models" do - assert_not_equal @user, Model::Event.create(:name => "Ruby Tuesday") - end - - should "be comparable to non-models" do - assert_not_equal @user, 1 - assert_not_equal @user, true - - # Not equal although the other object responds to #key. - assert_not_equal @user, OpenStruct.new(:key => @user.send(:key)) - end - end - - context "Debugging" do - class ::Model::Bar < Ohm::Model - attribute :name - counter :visits - set :friends - list :comments - - def foo - bar.foo - end - - def baz - bar.new.foo - end - - def bar - SomeMissingConstant - end - end - - should "provide a meaningful inspect" do - bar = Model::Bar.new - - assert_equal "#", bar.inspect - - bar.update(:name => "Albert") - bar.friends << 1 - bar.friends << 2 - bar.comments << "A" - bar.incr(:visits) - - assert_equal %Q{# comments=# visits=1>}, Model::Bar[bar.id].inspect - end - - def assert_wrapper_exception(&block) - begin - block.call - rescue NoMethodError => exception_raised - end - - assert_match /You tried to call SomeMissingConstant#\w+, but SomeMissingConstant is not defined on #{__FILE__}:\d+:in `bar'/, exception_raised.message - end - - should "inform about a miscatch by Wrapper when calling class methods" do - assert_wrapper_exception { Model::Bar.new.baz } - end - - should "inform about a miscatch by Wrapper when calling instance methods" do - assert_wrapper_exception { Model::Bar.new.foo } - end - end - - context "Overwriting write" do - class ::Model::Baz < Ohm::Model - attribute :name - - def write - self.name = "Foobar" - super - end - end - - should "work properly" do - baz = Model::Baz.new - baz.name = "Foo" - baz.save - baz.name = "Foo" - baz.save - assert_equal "Foobar", Model::Baz[baz.id].name - end - end - - context "References to other objects" do - class ::Model::Comment < Ohm::Model - end - - class ::Model::Rating < Ohm::Model - end - - class ::Model::Note < Ohm::Model - attribute :content - reference :source, Model::Post - collection :comments, Model::Comment - list :ratings, Model::Rating - end - - class ::Model::Comment - reference :note, Model::Note - end - - class ::Model::Rating - attribute :value - end - - class ::Model::Editor < Ohm::Model - attribute :name - reference :post, Model::Post - end - - class ::Model::Post < Ohm::Model - reference :author, Model::Person - collection :notes, Model::Note, :source - collection :editors, Model::Editor - end - - setup do - @post = Model::Post.create - end - - context "a reference to another object" do - should "return an instance of Person if author_id has a valid id" do - @post.author_id = Model::Person.create(:name => "Albert").id - @post.save - assert_equal "Albert", Model::Post[@post.id].author.name - end - - should "assign author_id if author is sent a valid instance" do - @post.author = Model::Person.create(:name => "Albert") - @post.save - assert_equal "Albert", Model::Post[@post.id].author.name - end - - should "assign nil if nil is passed to author" do - @post.author = nil - @post.save - assert_nil Model::Post[@post.id].author - end - - should "be cached in an instance variable" do - @author = Model::Person.create(:name => "Albert") - @post.update(:author => @author) - - assert_equal @author, @post.author - assert @post.author.object_id == @post.author.object_id - - @post.update(:author => Model::Person.create(:name => "Bertrand")) - - assert_equal "Bertrand", @post.author.name - assert @post.author.object_id == @post.author.object_id - - @post.update(:author_id => Model::Person.create(:name => "Charles").id) - - assert_equal "Charles", @post.author.name - end - end - - context "a collection of other objects" do - setup do - @note = Model::Note.create(:content => "Interesting stuff", :source => @post) - @comment = Model::Comment.create(:note => @note) - end - - should "return a set of notes" do - assert_equal @note.source, @post - assert_equal @note, @post.notes.first - end - - should "return a set of comments" do - assert_equal @comment, @note.comments.first - end - - should "return a list of ratings" do - @rating = Model::Rating.create(:value => 5) - @note.ratings << @rating - - assert_equal @rating, @note.ratings.first - end - - should "default to the current class name" do - @editor = Model::Editor.create(:name => "Albert", :post => @post) - - assert_equal @editor, @post.editors.first - end - end - end - - context "Models connected to different databases" do - class ::Model::Car < Ohm::Model - attribute :name - end - - class ::Model::Make < Ohm::Model - attribute :name - end - - setup do - Model::Car.connect(:port => 6379, :db => 14) - end - - teardown do - Model::Car.db.flushdb - end - - should "save to the selected database" do - car = Model::Car.create(:name => "Twingo") - make = Model::Make.create(:name => "Renault") - - assert_equal ["1"], Redis.new(:db => 15).smembers("Model::Make:all") - assert_equal [], Redis.new(:db => 15).smembers("Model::Car:all") - - assert_equal ["1"], Redis.new(:db => 14).smembers("Model::Car:all") - assert_equal [], Redis.new(:db => 14).smembers("Model::Make:all") - - assert_equal car, Model::Car[1] - assert_equal make, Model::Make[1] - - Model::Make.db.flushdb - - assert_equal car, Model::Car[1] - assert_nil Model::Make[1] - end - end -end diff --git a/test/model_test.rb b/test/model_test.rb index 55a5c3d..ee144e5 100644 --- a/test/model_test.rb +++ b/test/model_test.rb @@ -6,7 +6,6 @@ class Post < Ohm::Model attribute :body - list :comments list :related, Post end @@ -41,6 +40,12 @@ def write end end +module SomeNamespace + class Foo < Ohm::Model + attribute :name + end +end + class ModelTest < Test::Unit::TestCase setup do Ohm.flush @@ -130,8 +135,8 @@ class RedefinedModel < Ohm::Model should "not raise if a list is redefined" do assert_nothing_raised do class RedefinedModel < Ohm::Model - list :todo - list :todo + list :todo, lambda { } + list :todo, lambda { } end end end @@ -139,8 +144,8 @@ class RedefinedModel < Ohm::Model should "not raise if a set is redefined" do assert_nothing_raised do class RedefinedModel < Ohm::Model - set :friends - set :friends + set :friends, lambda { } + set :friends, lambda { } end end end @@ -148,8 +153,8 @@ class RedefinedModel < Ohm::Model should "not raise if a collection is redefined" do assert_nothing_raised do class RedefinedModel < Ohm::Model - list :toys - set :toys + list :toys, lambda { } + set :toys, lambda { } end end end @@ -263,14 +268,14 @@ class RedefinedModel < Ohm::Model should "delete an existing model" do class ModelToBeDeleted < Ohm::Model attribute :name - set :foos - list :bars + set :foos, Post + list :bars, Post end @model = ModelToBeDeleted.create(:name => "Lorem") - @model.foos << "foo" - @model.bars << "bar" + @model.foos << Post.create + @model.bars << Post.create id = @model.id @@ -463,90 +468,90 @@ class ::Foo < Ohm::Model end should "return an array" do - assert @post.comments.all.kind_of?(Array) + assert @post.related.all.kind_of?(Array) end should "append elements with push" do - @post.comments.push "1" - @post.comments << "2" + @post.related.push Post.create + @post.related << Post.create - assert_equal ["1", "2"], @post.comments.all + assert_equal ["2", "3"], @post.related.all.map { |model| model.id } end should "keep the inserting order" do - @post.comments << "1" - @post.comments << "2" - @post.comments << "3" - assert_equal ["1", "2", "3"], @post.comments.all + @post.related << Post.create + @post.related << Post.create + @post.related << Post.create + assert_equal ["2", "3", "4"], @post.related.all.map { |model| model.id } end should "keep the inserting order after saving" do - @post.comments << "1" - @post.comments << "2" - @post.comments << "3" + @post.related << Post.create + @post.related << Post.create + @post.related << Post.create @post.save - assert_equal ["1", "2", "3"], Post[@post.id].comments.all + assert_equal ["2", "3", "4"], Post[@post.id].related.map { |model| model.id } end should "respond to each" do - @post.comments << "1" - @post.comments << "2" - @post.comments << "3" + @post.related << Post.create + @post.related << Post.create + @post.related << Post.create - i = 1 - @post.comments.each do |c| - assert_equal i, c.to_i + i = 2 + @post.related.each do |c| + assert_equal i, c.id.to_i i += 1 end end should "return the size of the list" do - @post.comments << "1" - @post.comments << "2" - @post.comments << "3" - assert_equal 3, @post.comments.size + @post.related << Post.create + @post.related << Post.create + @post.related << Post.create + assert_equal 3, @post.related.size end should "return the last element with pop" do - @post.comments << "1" - @post.comments << "2" - assert_equal "2", @post.comments.pop - assert_equal "1", @post.comments.pop - assert @post.comments.empty? + @post.related << Post.create + @post.related << Post.create + assert_equal "3", @post.related.pop.id + assert_equal "2", @post.related.pop.id + assert @post.related.empty? end should "return the first element with shift" do - @post.comments << "1" - @post.comments << "2" - assert_equal "1", @post.comments.shift - assert_equal "2", @post.comments.shift - assert @post.comments.empty? + @post.related << Post.create + @post.related << Post.create + assert_equal "2", @post.related.shift.id + assert_equal "3", @post.related.shift.id + assert @post.related.empty? end should "push to the head of the list with unshift" do - @post.comments.unshift "1" - @post.comments.unshift "2" - assert_equal "1", @post.comments.pop - assert_equal "2", @post.comments.pop - assert @post.comments.empty? + @post.related.unshift Post.create + @post.related.unshift Post.create + assert_equal "2", @post.related.pop.id + assert_equal "3", @post.related.pop.id + assert @post.related.empty? end should "empty the list" do - @post.comments.unshift "1" - @post.comments.clear + @post.related.unshift Post.create + @post.related.clear - assert @post.comments.empty? + assert @post.related.empty? end should "replace the values in the list" do - @post.comments.replace(["1", "2"]) + @post.related.replace([Post.create, Post.create]) - assert_equal ["1", "2"], @post.comments + assert_equal ["2", "3"], @post.related.map { |model| model.id } end should "add models" do @post.related.add(Post.create(:body => "Hello")) - assert_equal ["2"], @post.related.ids + assert_equal ["2"], @post.related.map { |model| model.id } end should "find elements in the list" do @@ -562,7 +567,7 @@ class ::Foo < Ohm::Model @post.related.unshift(Post.create(:body => "Hello")) @post.related.unshift(Post.create(:body => "Goodbye")) - assert_equal ["3", "2"], @post.related.ids + assert_equal ["3", "2"], @post.related.map { |model| model.id } assert_equal "3", @post.related.shift.id @@ -603,8 +608,8 @@ class ::Appointment < Ohm::Model setup do @calendar = Calendar.create - @calendar.holidays.ids << "2009-05-25" - @calendar.holidays.ids << "2009-07-09" + @calendar.holidays.ids.rpush "2009-05-25" + @calendar.holidays.ids.rpush "2009-07-09" @calendar.subscribers << MyActiveRecordModel.find(1) end @@ -612,7 +617,7 @@ class ::Appointment < Ohm::Model should "apply a transformation" do assert_equal [Date.new(2009, 5, 25), Date.new(2009, 7, 9)], @calendar.holidays.all - assert_equal ["1"], @calendar.subscribers.ids.all + assert_equal [1], @calendar.subscribers.all.map { |model| model.id } assert_equal [MyActiveRecordModel.find(1)], @calendar.subscribers.all end @@ -631,13 +636,13 @@ class ::Appointment < Ohm::Model context "Sorting lists and sets" do setup do @post = Post.create(:body => "Lorem") - @post.comments << 2 - @post.comments << 3 - @post.comments << 1 + @post.related << Post.create + @post.related << Post.create + @post.related << Post.create end should "sort values" do - assert_equal %w{1 2 3}, @post.comments.sort + assert_equal %w{2 3 4}, @post.related.sort.map { |model| model.id } end end @@ -741,8 +746,8 @@ class ::Appointment < Ohm::Model class ::Bar < Ohm::Model attribute :name counter :visits - set :friends - list :comments + set :friends, self + list :comments, self def foo bar.foo @@ -763,12 +768,12 @@ def bar assert_equal "#", bar.inspect bar.update(:name => "Albert") - bar.friends << 1 - bar.friends << 2 - bar.comments << "A" + bar.friends << Bar.create + bar.friends << Bar.create + bar.comments << Bar.create bar.incr(:visits) - assert_equal %Q{# comments=# visits=1>}, Bar[bar.id].inspect + assert_equal %Q{# comments=# visits=1>}, Bar[bar.id].inspect end def assert_wrapper_exception(&block) @@ -975,7 +980,7 @@ def validate assert_equal({}, person.to_hash) end end - + context "a new model with some errors" do should "export a hash with the errors" do person = Person.new @@ -991,13 +996,13 @@ def validate assert_equal({ :id => '1' }, person.to_hash) end end - + context "an existing model with validation errors" do should "export a hash with its id and the errors" do person = Person.create(:name => "John Doe") person.name = nil person.valid? - + expected_hash = { :id => '1', :errors => [[:name, :not_present]] } assert_equal expected_hash, person.to_hash @@ -1034,4 +1039,14 @@ def to_hash end end end + + context "namespaced models" do + should "be persisted" do + SomeNamespace::Foo.create(:name => "foo") + + assert_equal "hash", Ohm.redis.type("SomeNamespace::Foo:1") + + assert_equal "foo", SomeNamespace::Foo[1].name + end + end end