ricordami 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/CHANGELOG.md +13 -0
  2. data/ISSUES.md +0 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.md +454 -0
  5. data/TODO.md +21 -0
  6. data/examples/calls.rb +64 -0
  7. data/examples/singers.rb +42 -0
  8. data/lib/ricordami/attribute.rb +52 -0
  9. data/lib/ricordami/can_be_queried.rb +133 -0
  10. data/lib/ricordami/can_be_validated.rb +27 -0
  11. data/lib/ricordami/can_have_relationships.rb +152 -0
  12. data/lib/ricordami/configuration.rb +39 -0
  13. data/lib/ricordami/connection.rb +22 -0
  14. data/lib/ricordami/exceptions.rb +14 -0
  15. data/lib/ricordami/has_attributes.rb +159 -0
  16. data/lib/ricordami/has_indices.rb +105 -0
  17. data/lib/ricordami/is_lockable.rb +52 -0
  18. data/lib/ricordami/is_persisted.rb +123 -0
  19. data/lib/ricordami/is_retrievable.rb +35 -0
  20. data/lib/ricordami/key_namer.rb +63 -0
  21. data/lib/ricordami/model.rb +29 -0
  22. data/lib/ricordami/query.rb +68 -0
  23. data/lib/ricordami/relationship.rb +40 -0
  24. data/lib/ricordami/unique_index.rb +59 -0
  25. data/lib/ricordami/unique_validator.rb +21 -0
  26. data/lib/ricordami/value_index.rb +26 -0
  27. data/lib/ricordami/version.rb +3 -0
  28. data/lib/ricordami.rb +26 -0
  29. data/spec/acceptance/manage_relationships_spec.rb +42 -0
  30. data/spec/acceptance/model_with_validation_spec.rb +78 -0
  31. data/spec/acceptance/query_model_spec.rb +93 -0
  32. data/spec/acceptance_helper.rb +2 -0
  33. data/spec/ricordami/attribute_spec.rb +113 -0
  34. data/spec/ricordami/can_be_queried_spec.rb +254 -0
  35. data/spec/ricordami/can_be_validated_spec.rb +115 -0
  36. data/spec/ricordami/can_have_relationships_spec.rb +255 -0
  37. data/spec/ricordami/configuration_spec.rb +45 -0
  38. data/spec/ricordami/connection_spec.rb +25 -0
  39. data/spec/ricordami/exceptions_spec.rb +43 -0
  40. data/spec/ricordami/has_attributes_spec.rb +266 -0
  41. data/spec/ricordami/has_indices_spec.rb +73 -0
  42. data/spec/ricordami/is_lockable_spec.rb +45 -0
  43. data/spec/ricordami/is_persisted_spec.rb +186 -0
  44. data/spec/ricordami/is_retrievable_spec.rb +55 -0
  45. data/spec/ricordami/key_namer_spec.rb +56 -0
  46. data/spec/ricordami/model_spec.rb +65 -0
  47. data/spec/ricordami/query_spec.rb +156 -0
  48. data/spec/ricordami/relationship_spec.rb +123 -0
  49. data/spec/ricordami/unique_index_spec.rb +87 -0
  50. data/spec/ricordami/unique_validator_spec.rb +41 -0
  51. data/spec/ricordami/value_index_spec.rb +40 -0
  52. data/spec/spec_helper.rb +29 -0
  53. data/spec/support/constants.rb +43 -0
  54. data/spec/support/db_manager.rb +18 -0
  55. data/test/bin/data_loader.rb +107 -0
  56. data/test/data/domains.txt +462 -0
  57. data/test/data/first_names.txt +1220 -0
  58. data/test/data/last_names.txt +1028 -0
  59. data/test/data/people_100_000.csv.bz2 +0 -0
  60. data/test/data/people_10_000.csv.bz2 +0 -0
  61. data/test/data/people_1_000_000.csv.bz2 +0 -0
  62. metadata +258 -0
@@ -0,0 +1,123 @@
1
+ require "spec_helper"
2
+ require "ricordami/relationship"
3
+
4
+ describe Ricordami::Relationship do
5
+ def valid_options
6
+ {:other => :blih, :self => :blah}
7
+ end
8
+ uses_constants("Stuff")
9
+ subject { Ricordami::Relationship }
10
+
11
+ describe "using most options" do
12
+ subject do
13
+ Ricordami::Relationship.new(:references_many, :other => :stuffs,
14
+ :as => :things, :self => :person,
15
+ :alias => :owner, :dependent => :delete)
16
+ end
17
+
18
+ it("has a type") { subject.type.should == :references_many }
19
+
20
+ it("has a name") { subject.name.should == :things }
21
+
22
+ it("has an object kind") { subject.object_kind.should == :stuff }
23
+
24
+ it("returns the object class") { subject.object_class.should == Stuff }
25
+
26
+ it("has a self kind") { subject.self_kind.should == :person }
27
+
28
+ it("has an alias") { subject.alias.should == :owner }
29
+
30
+ it("has a referrer id") { subject.referrer_id.should == "owner_id" }
31
+
32
+ it("has a dependent attribute") { subject.dependent.should == :delete }
33
+ end
34
+
35
+ describe "other cases" do
36
+ it "can specify an optional dependent option" do
37
+ relationship = subject.new(:references_many, valid_options.merge(:dependent => :delete))
38
+ relationship.dependent.should == :delete
39
+ end
40
+
41
+ it "deducts the object kind from the other parameter" do
42
+ relationship = subject.new(:references_many, valid_options.merge(:other => :stuffs))
43
+ relationship.name.should == :stuffs
44
+ relationship.object_kind.should == :stuff
45
+ end
46
+
47
+ it "can specify an optional as option that is the relationship name" do
48
+ relationship = subject.new(:references_many, valid_options.merge(:other => :stuffs, :as => :things))
49
+ relationship.name.should == :things
50
+ relationship.object_kind.should == :stuff
51
+ end
52
+
53
+ it "deducts the referrer id from the other or as parameter for a referenced_in relationship" do
54
+ relationship = subject.new(:referenced_in, :self => :ingredient, :other => :cook)
55
+ relationship.referrer_id.should == "cook_id"
56
+ relationship = subject.new(:referenced_in, :self => :ingredient, :other => :cook, :as => :chef)
57
+ relationship.referrer_id.should == "chef_id"
58
+ end
59
+
60
+ it "deducts the referrer id from the self or alias parameter for a references_many relationship" do
61
+ relationship = subject.new(:references_many, :self => :cook, :other => :ingredients)
62
+ relationship.referrer_id.should == "cook_id"
63
+ relationship = subject.new(:references_many, :self => :cook, :alias => :chef, :other => :ingredients, :as => :stuff)
64
+ relationship.referrer_id.should == "chef_id"
65
+ end
66
+
67
+ it "deducts the referrer id from the self or alias parameter for a references_one relationship" do
68
+ relationship = subject.new(:references_one, :self => :cook, :other => :hat)
69
+ relationship.referrer_id.should == "cook_id"
70
+ relationship = subject.new(:references_one, :self => :cook, :alias => :chef, :other => :hat, :as => :toque)
71
+ relationship.referrer_id.should == "chef_id"
72
+ end
73
+
74
+ it "can specify an optional alias that is the relationship name for the other party" do
75
+ relationship = subject.new(:references_many, valid_options.merge(:self => :person, :alias => :owners))
76
+ relationship.alias.should == :owners
77
+ relationship.self_kind.should == :person
78
+ end
79
+ end
80
+
81
+ describe "error cases" do
82
+ it "raises an error if all the mandatory parameters are not present" do
83
+ [{}, {:other => :blah}, {:self => :blih}].each do |options|
84
+ lambda {
85
+ subject.new(:references_many, options)
86
+ }.should raise_error(Ricordami::MissingMandatoryArgs)
87
+ end
88
+ end
89
+
90
+ it "doesn't raise an error if all mandatory parameters are present" do
91
+ lambda {
92
+ subject.new(:referenced_in, :other => :blah, :self => :blih)
93
+ }.should_not raise_error
94
+ end
95
+
96
+ it "raises an error if the type is not supported" do
97
+ lambda { subject.new(:references_many, valid_options) }.should_not raise_error
98
+ lambda { subject.new(:referenced_in, valid_options) }.should_not raise_error
99
+ lambda { subject.new(:references_one, valid_options) }.should_not raise_error
100
+ lambda {
101
+ subject.new(:blah, valid_options)
102
+ }.should raise_error(Ricordami::TypeNotSupported)
103
+ end
104
+
105
+ it "raises an error if dependent is set but not equal to :nullify or :delete" do
106
+ lambda {
107
+ subject.new(:references_many, valid_options.merge(:dependent => :nullify))
108
+ }.should_not raise_error
109
+ lambda {
110
+ subject.new(:references_many, valid_options.merge(:dependent => :delete))
111
+ }.should_not raise_error
112
+ lambda {
113
+ subject.new(:references_many, valid_options.merge(:dependent => :what))
114
+ }.should raise_error(Ricordami::OptionValueInvalid)
115
+ end
116
+
117
+ it "raises an error if an option is not supported" do
118
+ lambda {
119
+ subject.new(:references_many, valid_options.merge(:not_supported => :blah))
120
+ }.should raise_error(ArgumentError)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,87 @@
1
+ require "spec_helper"
2
+
3
+ describe Ricordami::UniqueIndex do
4
+ subject { Ricordami::UniqueIndex }
5
+
6
+ before(:each) do
7
+ create_constant("DataSource")
8
+ DataSource.attribute :name
9
+ @index = subject.new(DataSource, :id)
10
+ end
11
+
12
+ it "is initialized with a model, a name and the fields to be unique" do
13
+ @index.model.should == DataSource
14
+ @index.fields.should == [:id]
15
+ @index.name.should == :id
16
+ end
17
+
18
+ it "returns its internal index name with #uidx_key_name" do
19
+ @index.uidx_key_name.should == "DataSource:udx:id"
20
+ end
21
+
22
+ it "returns its internal reference name with #ref_key_name" do
23
+ @index.ref_key_name.should == "DataSource:hsh:id_to_id"
24
+ end
25
+
26
+ it "adds a string to the index with #add" do
27
+ @index.add("ze-id", "allo")
28
+ Ricordami.driver.smembers("DataSource:udx:id").should == ["allo"]
29
+ end
30
+
31
+ it "also indices the hash index with #add if fields is not :id and :get_by is true" do
32
+ DataSource.attribute :domain
33
+ other = subject.new(DataSource, [:name, :domain], :get_by => true)
34
+ other.add("ze-id", ["jobs", "apple.com"])
35
+ Ricordami.driver.smembers("DataSource:udx:name_domain").should == ["jobs_-::-_apple.com"]
36
+ Ricordami.driver.hget("DataSource:hsh:name_domain_to_id", "jobs_-::-_apple.com").should == "ze-id"
37
+ end
38
+
39
+ it "doesn't index the has index with #add if :get_by is false or fields is :id" do
40
+ one = subject.new(DataSource, :name)
41
+ one.add("ze-id", "jobs")
42
+ Ricordami.driver.hexists("DataSource:hsh:name_to_id", "jobs").should be_false
43
+ two = subject.new(DataSource, :id, :get_by => true)
44
+ two.add("ze-id", "ze-id")
45
+ Ricordami.driver.hexists("DataSource:hsh:id_to_id", "ze-id").should be_false
46
+ three = subject.new(DataSource, :name, :get_by => true)
47
+ three.add("ze-id", "jobs")
48
+ Ricordami.driver.hexists("DataSource:hsh:name_to_id", "jobs").should be_true
49
+ end
50
+
51
+ it "returns the id from values with #id_for_values if :get_by is true" do
52
+ DataSource.attribute :domain
53
+ two_fields = subject.new(DataSource, [:name, :domain], :get_by => true)
54
+ two_fields.add("ze-id", ["jobs", "apple.com"])
55
+ two_fields.id_for_values("jobs", "apple.com").should == "ze-id"
56
+ one_field = subject.new(DataSource, :name, :get_by => true)
57
+ one_field.add("ze-id", "jobs")
58
+ one_field.id_for_values("jobs").should == "ze-id"
59
+ end
60
+
61
+ it "removes a string from the index with #rem" do
62
+ @index.add("ze-id", "allo")
63
+ @index.rem("ze-id", "allo")
64
+ Ricordami.driver.smembers("DataSource:udx:id").should == []
65
+ end
66
+
67
+ it "returns the redis command(s) to remove the value from the index when return_command is true" do
68
+ command = @index.rem("ze-id", "allo", true)
69
+ command.should == [[:srem, ["DataSource:udx:id", "allo"]]]
70
+ end
71
+
72
+ it "returns the number of entries with #count" do
73
+ 5.times { |i| @index.add("ze-id", i.to_s) }
74
+ @index.count.should == 5
75
+ end
76
+
77
+ it "returns all the strings from the index with #all" do
78
+ %w(allo la terre).each { |v| @index.add("ze-id", v) }
79
+ @index.all.should =~ ["allo", "la", "terre"]
80
+ end
81
+
82
+ it "returns if a string already exists with #include?" do
83
+ %w(allo la terre).each { |v| @index.add("ze-id", v) }
84
+ @index.should include("terre")
85
+ @index.should_not include("earth")
86
+ end
87
+ end
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+ require "ricordami/can_be_validated"
3
+
4
+ describe Ricordami::UniqueValidator do
5
+ uses_constants("Call")
6
+
7
+ before(:each) do
8
+ Call.send(:include, Ricordami::CanBeValidated)
9
+ Call.attribute :name
10
+ end
11
+ let(:record) { Call.new(:name => "sophie") }
12
+ let(:validator) { Ricordami::UniqueValidator.new(:attributes => [:name]) }
13
+
14
+ it "is an active model EachValidator" do
15
+ validator.is_a?(ActiveModel::EachValidator)
16
+ end
17
+
18
+ it "#setup adds a unique index" do
19
+ validator.setup(Call)
20
+ Call.indices[:name].should be_a(Ricordami::UniqueIndex)
21
+ end
22
+
23
+ it "#validate_each adds an error if the value is already used" do
24
+ validator.setup(Call)
25
+ record.save
26
+
27
+ sophie = Call.new(:name => "sophie")
28
+ validator.validate_each(sophie, :name, record.name)
29
+ sophie.should have(1).error
30
+ sophie.errors[:name].should == ["is already used"]
31
+ end
32
+
33
+ it "accepts an option :message to change the error message" do
34
+ validator = Ricordami::UniqueValidator.new(:attributes => [:name], :message => "come on, man!")
35
+ validator.setup(Call)
36
+ record.save
37
+ sophie = Call.new(:name => "sophie")
38
+ validator.validate_each(sophie, :name, record.name)
39
+ sophie.errors[:name].should == ["come on, man!"]
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ require "spec_helper"
2
+ require "ricordami/value_index"
3
+
4
+ describe Ricordami::ValueIndex do
5
+ subject { Ricordami::ValueIndex }
6
+
7
+ before(:each) do
8
+ create_constant("Friend")
9
+ Friend.attribute :first_name
10
+ @index = subject.new(Friend, :first_name)
11
+ end
12
+ let(:index) { @index }
13
+
14
+ it "is initialized with a model, a name and a field" do
15
+ index.model.should == Friend
16
+ index.field.should == :first_name
17
+ index.name.should == :first_name
18
+ end
19
+
20
+ it "has a key name for each distinct value with #key_name_for_value" do
21
+ index.key_name_for_value("VALUE").should == "Friend:idx:first_name:VkFMVUU="
22
+ end
23
+
24
+ it "adds the id to the index value with #add" do
25
+ index.add("3", "VALUE")
26
+ Ricordami.driver.smembers("Friend:idx:first_name:VkFMVUU=").should == ["3"]
27
+ end
28
+
29
+ it "removes the id from the index value with #rem" do
30
+ index.add("1", "VALUE")
31
+ index.add("2", "VALUE")
32
+ index.rem("1", "VALUE")
33
+ Ricordami.driver.smembers("Friend:idx:first_name:VkFMVUU=").should == ["2"]
34
+ end
35
+
36
+ it "returns the redis command to remove the value from the index when return_command is true" do
37
+ commands = index.rem("1", "VALUE", true)
38
+ commands.should == [[:srem, ["Friend:idx:first_name:VkFMVUU=", "1"]]]
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ spec_dir = Pathname.new(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift spec_dir
3
+
4
+ require "rubygems"
5
+ require "bundler"
6
+
7
+ Bundler.setup :default, :test
8
+ ENV['RACK_ENV'] ||= "test"
9
+
10
+ require "rspec"
11
+ require "support/constants"
12
+ require "support/db_manager"
13
+ require "awesome_print"
14
+
15
+ RSpec.configure do |config|
16
+ config.include Support::Constants
17
+ config.include Support::DbManager
18
+ config.before(:each) do
19
+ Ricordami.configure do |config|
20
+ config.from_hash(:redis_host => "127.0.0.1",
21
+ :redis_port => 6379,
22
+ :redis_db => 7,
23
+ :thread_safe => true)
24
+ end
25
+ Ricordami.driver.flushdb
26
+ end
27
+ end
28
+
29
+ require spec_dir + "../lib/ricordami"
@@ -0,0 +1,43 @@
1
+ # copied from toystore gem (https://github.com/newtoy/toystore/raw/master/spec/support/constants.rb)
2
+ # and a bit updated for ricordami
3
+
4
+ module Support
5
+ module Constants
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def uses_constants(*constants)
12
+ before { create_constants(*constants) }
13
+ end
14
+ end
15
+
16
+ def create_constants(*constants)
17
+ constants.each { |constant| create_constant(constant) }
18
+ end
19
+
20
+ def remove_constants(*constants)
21
+ constants.each { |constant| remove_constant(constant) }
22
+ end
23
+
24
+ def create_constant(constant)
25
+ remove_constant(constant)
26
+ Object.const_set(constant, Model(constant))
27
+ end
28
+
29
+ def remove_constant(constant)
30
+ Object.send(:remove_const, constant) if Object.const_defined?(constant)
31
+ end
32
+
33
+ def Model(name=nil)
34
+ Class.new.tap do |model|
35
+ model.class_eval """
36
+ def self.name; '#{name}' end
37
+ def self.to_s; '#{name}' end
38
+ """ if name
39
+ model.send(:include, Ricordami::Model)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ module Support
2
+ module DbManager
3
+ def switch_db_to_error
4
+ @_db_mode_error = true
5
+ error = Object.new
6
+ def error.method_missing(meth, *args, &blk)
7
+ raise Errno::ECONNREFUSED
8
+ end
9
+ Ricordami.instance_eval { @driver = error }
10
+ end
11
+
12
+ def switch_db_to_ok
13
+ return unless @_db_mode_error
14
+ Ricordami.instance_eval { @driver = nil }
15
+ @_db_mode_error = false
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+ Bundler.setup :default, :development
6
+
7
+ require "thor"
8
+ require "thor/actions"
9
+ require "csv"
10
+ require "redis"
11
+
12
+ class DataLoader < Thor
13
+ include Thor::Actions
14
+
15
+ add_runtime_options!
16
+ check_unknown_options!
17
+
18
+ desc "generate_people FILE_NAME", "Generate a CSV file of random people"
19
+ method_option :number, :aliases => "-n", :default => 1_000, :type => :numeric, :desc => "number of people to generate"
20
+
21
+ def generate_people(file_name)
22
+ d = load_data
23
+ CSV.open(file_name, "w") do |csv|
24
+ csv << %w(first_name last_name email age)
25
+ options[:number].times do
26
+ f, l = get_first_name(d), get_last_name(d)
27
+ csv << [f, l, get_email(d, f, l), get_age]
28
+ end
29
+ end
30
+ puts "File #{file_name} generated with #{options[:number]} people."
31
+ end
32
+
33
+ desc "load_people FILE_NAME", "Load a people file into Redis"
34
+ method_option :db, :aliases => "-d", :default => 1, :type => :numeric, :desc => "database number"
35
+
36
+ def load_people(file_name)
37
+ r = Redis.new
38
+ r.select(options[:db])
39
+ r.flushdb
40
+ key_ids = "global:people:ids"
41
+ key_att = "people:"
42
+ key_seq = "global:seq_id"
43
+ i = 0
44
+ CSV.foreach(file_name, :headers => :first_row) do |row|
45
+ id = r.incr(key_seq)
46
+ r.multi
47
+ r.hmset("#{key_att}#{id}", *row.to_a.flatten)
48
+ r.sadd(key_ids, id)
49
+ r.exec
50
+ i += 1
51
+ if i == 100
52
+ putc "."
53
+ i = 0
54
+ end
55
+ end
56
+ puts
57
+ puts "Db loaded."
58
+ end
59
+
60
+ private
61
+
62
+ def load_data
63
+ data_dir = File.expand_path("../../data", __FILE__)
64
+ {
65
+ :first => read_entries(File.join(data_dir, "first_names.txt")),
66
+ :last => read_entries(File.join(data_dir, "last_names.txt")),
67
+ :domains => read_entries(File.join(data_dir, "domains.txt"))
68
+ }
69
+ end
70
+
71
+ def read_entries(path)
72
+ [].tap do |entries|
73
+ File.open(path, "r") do |f|
74
+ f.each_line { |line| entries.push(line.chomp.downcase.capitalize) }
75
+ end
76
+ end
77
+ end
78
+
79
+ def get_first_name(d)
80
+ d[:first].sample(rand(12) == 3 ? 2 : 1).join(" ")
81
+ end
82
+
83
+ def get_last_name(d)
84
+ d[:last].sample
85
+ end
86
+
87
+ def get_email(d, f, l)
88
+ one = f[0..(rand(2) - 2)].downcase
89
+ dot = rand(2) == 0 ? "" : "."
90
+ two = l.split.join("-").downcase
91
+ "#{one}#{dot}#{two}@#{d[:domains].sample}"
92
+ end
93
+
94
+ def get_age
95
+ 18 + rand(50)
96
+ end
97
+ end
98
+
99
+ require "rubygems" if RUBY_VERSION[0..2].to_f < 1.9
100
+
101
+ begin
102
+ DataLoader.start
103
+ rescue Exception => ex
104
+ STDERR.puts "#{File.basename(__FILE__)}: #{ex}"
105
+ raise
106
+ end
107
+