ricordami 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+