require_relative "helper" require "ostruct" class Post < Ohm::Model attribute :body attribute :published set :related, :Post end class User < Ohm::Model attribute :email set :posts, :Post end class Person < Ohm::Model attribute :name counter :logins index :initial def initial name[0, 1].upcase if name end end class Event < Ohm::Model attribute :name counter :votes set :attendees, :Person attribute :slug def save self.slug = name.to_s.downcase super end end module SomeNamespace class Foo < Ohm::Model attribute :name end class Bar < Ohm::Model reference :foo, 'SomeNamespace::Foo' end end class Meetup < Ohm::Model attribute :name attribute :location end test "booleans" do post = Post.new(body: true, published: false) post.save assert_equal true, post.body assert_equal false, post.published post = Post[post.id] assert_equal "true", post.body assert_equal nil, post.published end test "empty model is ok" do class Foo < Ohm::Model end Foo.create end test "counters are cleaned up during deletion" do e = Event.create(:name => "Foo") e.increment :votes, 10 assert_equal 10, e.votes e.delete assert_equal 0, Event.redis.call("EXISTS", e.key[:counters]) end test "get" do m = Meetup.create(:name => "Foo") m.name = "Bar" assert_equal "Foo", m.get(:name) assert_equal "Foo", m.name end test "set" do m = Meetup.create(:name => "Foo") m.set :name, "Bar" assert_equal "Bar", m.name m = Meetup[m.id] assert_equal "Bar", m.name # Deletes when value is nil. m.set :name, nil m = Meetup[m.id] assert_equal 0, Meetup.redis.call("HEXISTS", m.key, :name) end test "assign attributes from the hash" do event = Event.new(:name => "Ruby Tuesday") assert event.name == "Ruby Tuesday" end test "assign an ID and save the object" do event1 = Event.create(name: "Ruby Tuesday") event2 = Event.create(name: "Ruby Meetup") assert_equal "1", event1.id assert_equal "2", event2.id end test "updates attributes" do event = Meetup.create(:name => "Ruby Tuesday") event.update(:name => "Ruby Meetup") assert "Ruby Meetup" == event.name end test "reload attributes" do event1 = Meetup.create(:name => "Foo", :location => "Bar") event2 = Meetup[event1.id] assert_equal "Foo", event1.name assert_equal "Bar", event1.location assert_equal "Foo", event2.name assert_equal "Bar", event2.location event1.update(:name => nil) event2.load! assert_equal nil, event1.name assert_equal "Bar", event1.location assert_equal nil, event2.name assert_equal "Bar", event2.location end test "save the attributes in UTF8" do event = Meetup.create(:name => "32° Kisei-sen") assert "32° Kisei-sen" == Meetup[event.id].name end test "delete the attribute if set to nil" do event = Meetup.create(:name => "Ruby Tuesday", :location => "Los Angeles") assert "Los Angeles" == Meetup[event.id].location assert event.update(:location => nil) assert_equal nil, Meetup[event.id].location end test "not raise if an attribute is redefined" do class RedefinedModel < Ohm::Model attribute :name silence_warnings do attribute :name end end end test "not raise if a counter is redefined" do class RedefinedModel < Ohm::Model counter :age silence_warnings do counter :age end end end test "not raise if a set is redefined" do class RedefinedModel < Ohm::Model set :friends, lambda { } silence_warnings do set :friends, lambda { } end end end test "not raise if a collection is redefined" do class RedefinedModel < Ohm::Model set :toys, lambda { } silence_warnings do set :toys, lambda { } end end end test "not raise if a index is redefined" do class RedefinedModel < Ohm::Model attribute :color index :color index :color end end test "allow arbitrary IDs" do Event.create(:id => "abc123", :name => "Concert") assert Event.all.size == 1 assert Event["abc123"].name == "Concert" end test "forbid assignment of IDs on a new object" do event = Event.new(:name => "Concert") assert_raise(NoMethodError) do event.id = "abc123" end end setup do Ohm.redis.call("SADD", "Event:all", 1) Ohm.redis.call("HSET", "Event:1", "name", "Concert") end test "return an instance of Event" do assert Event[1].kind_of?(Event) assert 1 == Event[1].id assert "Concert" == Event[1].name end setup do Ohm.redis.call("SADD", "User:all", 1) Ohm.redis.call("HSET", "User:1", "email", "albert@example.com") end test "return an instance of User" do assert User[1].kind_of?(User) assert 1 == User[1].id assert "albert@example.com" == User[1].email end test "allow to map key to models" do assert [User[1]] == [1].map(&User) end setup do Ohm.redis.call("SADD", "User:all", 1) Ohm.redis.call("SET", "User:1:email", "albert@example.com") @user = User[1] end test "change its attributes" do @user.email = "maria@example.com" assert "maria@example.com" == @user.email end test "save the new values" do @user.email = "maria@example.com" @user.save @user.email = "maria@example.com" @user.save assert "maria@example.com" == User[1].email end test "assign a new id to the event" do event1 = Event.new event1.save event2 = Event.new event2.save assert !event1.new? assert !event2.new? assert_equal "1", event1.id assert_equal "2", event2.id end # Saving a model test "create the model if it is new" do event = Event.new(:name => "Foo").save assert "Foo" == Event[event.id].name end test "save it only if it was previously created" do event = Event.new event.name = "Lorem ipsum" event.save event.name = "Lorem" event.save assert "Lorem" == Event[event.id].name end test "allow to hook into save" do event = Event.create(:name => "Foo") assert "foo" == event.slug end test "save counters" do event = Event.create(:name => "Foo") event.increment(:votes) event.save assert_equal 1, Event[event.id].votes end # Delete test "delete an existing model" do class ModelToBeDeleted < Ohm::Model attribute :name set :foos, :Post set :bars, :Post end @model = ModelToBeDeleted.create(:name => "Lorem") @model.foos.add(Post.create) @model.bars.add(Post.create) id = @model.id @model.delete assert Ohm.redis.call("GET", ModelToBeDeleted.key[id]).nil? assert Ohm.redis.call("GET", ModelToBeDeleted.key[id][:name]).nil? assert Array.new == Ohm.redis.call("SMEMBERS", ModelToBeDeleted.key[id][:foos]) assert Array.new == Ohm.redis.call("LRANGE", ModelToBeDeleted.key[id][:bars], 0, -1) assert ModelToBeDeleted.all.empty? end setup do end test "no leftover keys" do class ::Foo < Ohm::Model attribute :name index :name track :notes end assert_equal [], Ohm.redis.call("KEYS", "*") Foo.create(:name => "Bar") expected = %w[Foo:1:_indices Foo:1 Foo:all Foo:id Foo:indices:name:Bar] assert_equal expected.sort, Ohm.redis.call("KEYS", "*").sort Foo[1].delete assert ["Foo:id"] == Ohm.redis.call("KEYS", "*") Foo.create(:name => "Baz") Ohm.redis.call("SET", Foo[2].key[:notes], "something") expected = %w[Foo:2:_indices Foo:2 Foo:all Foo:id Foo:indices:name:Baz Foo:2:notes] assert_equal expected.sort, Ohm.redis.call("KEYS", "*").sort Foo[2].delete assert ["Foo:id"] == Ohm.redis.call("KEYS", "*") end # Listing test "find all" do event1 = Event.new event1.name = "Ruby Meetup" event1.save event2 = Event.new event2.name = "Ruby Tuesday" event2.save all = Event.all assert all.detect {|e| e.name == "Ruby Meetup" } assert all.detect {|e| e.name == "Ruby Tuesday" } end # Fetching test "fetch ids" do event1 = Event.create(:name => "A") event2 = Event.create(:name => "B") assert_equal [event1, event2], Event.fetch([event1.id, event2.id]) end # Sorting test "sort all" do Person.create :name => "D" Person.create :name => "C" Person.create :name => "B" Person.create :name => "A" names = Person.all.sort_by(:name, :order => "ALPHA").map { |p| p.name } assert %w[A B C D] == names end test "return an empty array if there are no elements to sort" do assert [] == Person.all.sort_by(:name) end test "return the first element sorted by id when using first" do Person.create :name => "A" Person.create :name => "B" assert "A" == Person.all.first.name end test "return the first element sorted by name if first receives a sorting option" do Person.create :name => "B" Person.create :name => "A" assert "A" == Person.all.first(:by => :name, :order => "ALPHA").name end test "return attribute values when the get parameter is specified" do Person.create :name => "B" Person.create :name => "A" res = Person.all.sort_by(:name, :get => :name, :order => "ALPHA") assert_equal ["A", "B"], res end test "work on lists" do post = Post.create :body => "Hello world!" redis = Post.redis redis.call("RPUSH", post.related.key, Post.create(:body => "C").id) redis.call("RPUSH", post.related.key, Post.create(:body => "B").id) redis.call("RPUSH", post.related.key, Post.create(:body => "A").id) res = post.related.sort_by(:body, :order => "ALPHA ASC").map { |r| r.body } assert_equal ["A", "B", "C"], res end # Loading attributes setup do event = Event.new event.name = "Ruby Tuesday" event.save.id end test "load attributes as a strings" do event = Event.create(:name => 1) assert "1" == Event[event.id].name end # Enumerable indices class Entry < Ohm::Model attribute :tags index :tag def tag tags.split(/\s+/) end end setup do Entry.create(:tags => "foo bar baz") end test "finding by one entry in the enumerable" do |entry| assert Entry.find(:tag => "foo").include?(entry) assert Entry.find(:tag => "bar").include?(entry) assert Entry.find(:tag => "baz").include?(entry) end test "finding by multiple entries in the enumerable" do |entry| assert Entry.find(:tag => ["foo", "bar"]).include?(entry) assert Entry.find(:tag => ["bar", "baz"]).include?(entry) assert Entry.find(:tag => ["baz", "oof"]).empty? end # Attributes of type Set setup do @person1 = Person.create(:name => "Albert") @person2 = Person.create(:name => "Bertrand") @person3 = Person.create(:name => "Charles") @event = Event.new @event.name = "Ruby Tuesday" end test "filter elements" do @event.save @event.attendees.add(@person1) @event.attendees.add(@person2) assert [@person1] == @event.attendees.find(:initial => "A").to_a assert [@person2] == @event.attendees.find(:initial => "B").to_a assert [] == @event.attendees.find(:initial => "Z").to_a end test "delete elements" do @event.save @event.attendees.add(@person1) @event.attendees.add(@person2) assert_equal 2, @event.attendees.size @event.attendees.delete(@person2) assert_equal 1, @event.attendees.size end test "not be available if the model is new" do assert_raise Ohm::MissingID do @event.attendees end end test "remove an element if sent delete" do @event.save @event.attendees.add(@person1) @event.attendees.add(@person2) @event.attendees.add(@person3) assert_equal ["1", "2", "3"], Event.redis.call("SORT", @event.attendees.key) Event.redis.call("SREM", @event.attendees.key, @person2.id) assert_equal ["1", "3"], Event.redis.call("SORT", Event[@event.id].attendees.key) end test "return true if the set includes some member" do @event.save @event.attendees.add(@person1) @event.attendees.add(@person2) assert @event.attendees.include?(@person2) @event.attendees.include?(@person3) assert !@event.attendees.include?(@person3) end test "return instances of the passed model" do @event.save @event.attendees.add(@person1) assert [@person1] == @event.attendees.to_a assert @person1 == @event.attendees[@person1.id] end test "return the size of the set" do @event.save @event.attendees.add(@person1) @event.attendees.add(@person2) @event.attendees.add(@person3) assert 3 == @event.attendees.size end test "empty the set" do @event.save @event.attendees.add(@person1) Event.redis.call("DEL", @event.attendees.key) assert @event.attendees.empty? end test "replace the values in the set" do @event.save @event.attendees.add(@person1) assert [@person1] == @event.attendees.to_a @event.attendees.replace([@person2, @person3]) assert [@person2, @person3] == @event.attendees.to_a.sort_by(&:id) end # Sorting lists and sets by model attributes setup do @event = Event.create(:name => "Ruby Tuesday") {'D' => 4, 'C' => 2, 'B' => 5, 'A' => 3}.each_pair do |name, logins| person = Person.create(:name => name) person.increment :logins, logins @event.attendees.add(person) end end test "sort the model instances by the values provided" do people = @event.attendees.sort_by(:name, :order => "ALPHA") assert %w[A B C D] == people.map(&:name) end test "accept a number in the limit parameter" do people = @event.attendees.sort_by(:name, :limit => [0, 2], :order => "ALPHA") assert %w[A B] == people.map(&:name) end test "use the start parameter as an offset if the limit is provided" do people = @event.attendees.sort_by(:name, :limit => [1, 2], :order => "ALPHA") assert %w[B C] == people.map(&:name) end test "use counter attributes for sorting" do people = @event.attendees.sort_by(:logins, :limit => [0, 3], :order => "ALPHA") assert %w[C A D] == people.map(&:name) end test "use counter attributes for sorting with key option" do people = @event.attendees.sort_by(:logins, :get => :logins, :limit => [0, 3], :order => "ALPHA") assert %w[2 3 4] == people end # Collections initialized with a Model parameter setup do @user = User.create(:email => "albert@example.com") @user.posts.add(Post.create(:body => "D")) @user.posts.add(Post.create(:body => "C")) @user.posts.add(Post.create(:body => "B")) @user.posts.add(Post.create(:body => "A")) end test "return instances of the passed model" do assert Post == @user.posts.first.class end test "remove an object from the set" do post = @user.posts.first assert @user.posts.include?(post) User.redis.call("SREM", @user.posts.key, post.id) assert !@user.posts.include?(post) end test "remove an object id from the set" do post = @user.posts.first assert @user.posts.include?(post) User.redis.call("SREM", @user.posts.key, post.id) assert !@user.posts.include?(post) end # Counters setup do @event = Event.create(:name => "Ruby Tuesday") end test "be zero if not initialized" do assert 0 == @event.votes end test "be able to increment a counter" do @event.increment(:votes) assert 1 == @event.votes @event.increment(:votes, 2) assert 3 == @event.votes end test "be able to decrement a counter" do @event.decrement(:votes) assert @event.votes == -1 @event.decrement(:votes, 2) assert @event.votes == -3 end # Comparison setup do @user = User.create(:email => "foo") end test "be comparable to other instances" do assert @user == User[@user.id] assert @user != User.create assert User.new != User.new end test "not be comparable to instances of other models" do assert @user != Event.create(:name => "Ruby Tuesday") end test "be comparable to non-models" do assert @user != 1 assert @user != true # Not equal although the other object responds to #key. assert @user != OpenStruct.new(:key => @user.send(:key)) end # Debugging class ::Bar < Ohm::Model attribute :name counter :visits set :friends, self set :comments, self def foo bar.foo end def baz bar.new.foo end def bar SomeMissingConstant end end # Models connected to different databases class ::Car < Ohm::Model attribute :name self.redis = Redic.new end class ::Make < Ohm::Model attribute :name end setup do Car.redis.call("SELECT", 15) end test "save to the selected database" do car = Car.create(:name => "Twingo") make = Make.create(:name => "Renault") redis = Redic.new assert ["1"] == redis.call("SMEMBERS", "Make:all") assert [] == redis.call("SMEMBERS", "Car:all") assert ["1"] == Car.redis.call("SMEMBERS", "Car:all") assert [] == Car.redis.call("SMEMBERS", "Make:all") assert car == Car[1] assert make == Make[1] Make.redis.call("FLUSHDB") assert car == Car[1] assert Make[1].nil? end test "allow changing the database" do Car.create(:name => "Twingo") assert_equal ["1"], Car.redis.call("SMEMBERS", Car.all.key) Car.redis = Redic.new("redis://127.0.0.1:6379") assert_equal [], Car.redis.call("SMEMBERS", Car.all.key) Car.redis.call("SELECT", 15) assert_equal ["1"], Car.redis.call("SMEMBERS", Car.all.key) end # Persistence test "persist attributes to a hash" do event = Event.create(:name => "Redis Meetup") event.increment(:votes) assert "hash" == Ohm.redis.call("TYPE", "Event:1") expected= %w[Event:1 Event:1:counters Event:all Event:id] assert_equal expected, Ohm.redis.call("KEYS", "Event:*").sort assert "Redis Meetup" == Event[1].name assert 1 == Event[1].votes end # namespaced models test "be persisted" do SomeNamespace::Foo.create(:name => "foo") SomeNamespace::Bar.create(:foo => SomeNamespace::Foo[1]) assert "hash" == Ohm.redis.call("TYPE", "SomeNamespace::Foo:1") assert "foo" == SomeNamespace::Foo[1].name assert "foo" == SomeNamespace::Bar[1].foo.name end if RUBY_VERSION >= "2.0.0" test "typecast attributes" do class Option < Ohm::Model attribute :votes, lambda { |x| x.to_i } end option = Option.create :votes => 20 option.update(:votes => option.votes + 1) assert_equal 21, option.votes end test "poster-example for overriding writers" do silence_warnings do class Advertiser < Ohm::Model attribute :email def email=(e) attributes[:email] = e.to_s.downcase.strip end end end a = Advertiser.new(:email => " FOO@BAR.COM ") assert_equal "foo@bar.com", a.email end test "scripts are flushed" do m = Meetup.create(:name => "Foo") Meetup.redis.call("SCRIPT", "FLUSH") m.update(:name => "Bar") assert_equal m.name, Meetup[m.id].name end