perpetuity 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +3 -0
  6. data/Guardfile +24 -0
  7. data/README.md +142 -0
  8. data/Rakefile +6 -0
  9. data/lib/perpetuity/attribute.rb +17 -0
  10. data/lib/perpetuity/attribute_set.rb +23 -0
  11. data/lib/perpetuity/config.rb +11 -0
  12. data/lib/perpetuity/data_injectable.rb +21 -0
  13. data/lib/perpetuity/mapper.rb +195 -0
  14. data/lib/perpetuity/mongodb/query.rb +19 -0
  15. data/lib/perpetuity/mongodb/query_attribute.rb +49 -0
  16. data/lib/perpetuity/mongodb/query_expression.rb +55 -0
  17. data/lib/perpetuity/mongodb.rb +102 -0
  18. data/lib/perpetuity/retrieval.rb +82 -0
  19. data/lib/perpetuity/validations/length.rb +36 -0
  20. data/lib/perpetuity/validations/presence.rb +14 -0
  21. data/lib/perpetuity/validations/validation_set.rb +27 -0
  22. data/lib/perpetuity/validations.rb +1 -0
  23. data/lib/perpetuity/version.rb +3 -0
  24. data/lib/perpetuity.rb +23 -0
  25. data/perpetuity.gemspec +25 -0
  26. data/spec/perpetuity/attribute_set_spec.rb +19 -0
  27. data/spec/perpetuity/attribute_spec.rb +19 -0
  28. data/spec/perpetuity/config_spec.rb +13 -0
  29. data/spec/perpetuity/data_injectable_spec.rb +26 -0
  30. data/spec/perpetuity/mapper_spec.rb +51 -0
  31. data/spec/perpetuity/mongodb/query_attribute_spec.rb +42 -0
  32. data/spec/perpetuity/mongodb/query_expression_spec.rb +49 -0
  33. data/spec/perpetuity/mongodb/query_spec.rb +37 -0
  34. data/spec/perpetuity/mongodb_spec.rb +91 -0
  35. data/spec/perpetuity/retrieval_spec.rb +58 -0
  36. data/spec/perpetuity/validations/length_spec.rb +53 -0
  37. data/spec/perpetuity/validations/presence_spec.rb +30 -0
  38. data/spec/perpetuity/validations_spec.rb +87 -0
  39. data/spec/perpetuity_spec.rb +293 -0
  40. data/spec/test_classes.rb +93 -0
  41. metadata +181 -0
@@ -0,0 +1,82 @@
1
+ require 'perpetuity/data_injectable'
2
+ require 'perpetuity/mapper'
3
+
4
+ module Perpetuity
5
+ class Retrieval
6
+ include DataInjectable
7
+ include Enumerable
8
+ attr_accessor :sort_attribute, :sort_direction, :result_limit, :result_page, :quantity_per_page
9
+
10
+ def initialize klass, criteria, data_source = Perpetuity.configuration.data_source
11
+ @class = klass
12
+ @criteria = criteria
13
+ @data_source = data_source
14
+ end
15
+
16
+ def sort attribute=:name
17
+ retrieval = clone
18
+ retrieval.sort_attribute = attribute
19
+ retrieval.sort_direction = :ascending
20
+
21
+ retrieval
22
+ end
23
+
24
+ def reverse
25
+ retrieval = clone
26
+ retrieval.sort_direction = retrieval.sort_direction == :descending ? :ascending : :descending
27
+
28
+ retrieval
29
+ end
30
+
31
+ def page page
32
+ retrieval = clone
33
+ retrieval.result_page = page
34
+ retrieval.quantity_per_page = 20
35
+ retrieval
36
+ end
37
+
38
+ def per_page per
39
+ retrieval = clone
40
+ retrieval.quantity_per_page = per
41
+ retrieval
42
+ end
43
+
44
+ def each &block
45
+ to_a.each(&block)
46
+ end
47
+
48
+ def to_a
49
+ options = {
50
+ attribute: sort_attribute,
51
+ direction: sort_direction,
52
+ limit: result_limit || quantity_per_page,
53
+ page: result_page
54
+ }
55
+ results = @data_source.retrieve(@class, @criteria, options)
56
+ objects = []
57
+ results.each do |result|
58
+ object = @class.new
59
+ inject_data object, Mapper.new.unserialize(result)
60
+
61
+ objects << object
62
+ end
63
+
64
+ objects
65
+ end
66
+
67
+ def [] index
68
+ to_a[index]
69
+ end
70
+
71
+ def empty?
72
+ to_a.empty?
73
+ end
74
+
75
+ def limit lim
76
+ retrieval = clone
77
+ retrieval.result_limit = lim
78
+
79
+ retrieval
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,36 @@
1
+ module Perpetuity
2
+ module Validations
3
+ class Length
4
+ def initialize attribute, options
5
+ @attribute = attribute
6
+ @at_least = nil
7
+ @at_most = nil
8
+ options.each do |option, value|
9
+ send option, value
10
+ end
11
+ end
12
+
13
+ def pass? object
14
+ length = object.send(@attribute).length
15
+
16
+ return false unless @at_least.nil? or @at_least <= length
17
+ return false unless @at_most.nil? or @at_most >= length
18
+
19
+ true
20
+ end
21
+
22
+ def at_least value
23
+ @at_least = value
24
+ end
25
+
26
+ def at_most value
27
+ @at_most = value
28
+ end
29
+
30
+ def between range
31
+ at_least range.min
32
+ at_most range.max
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ module Perpetuity
2
+ module Validations
3
+ class Presence
4
+ def initialize attribute
5
+ @attribute = attribute
6
+ end
7
+
8
+ def pass? object
9
+ !object.send(@attribute).nil? &&
10
+ object.send(@attribute).strip != ''
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ require 'perpetuity/validations/length'
2
+ require 'perpetuity/validations/presence'
3
+
4
+ module Perpetuity
5
+ class ValidationSet < Set
6
+
7
+ def valid? object
8
+ each do |validation|
9
+ return false unless validation.pass?(object)
10
+ end
11
+
12
+ true
13
+ end
14
+
15
+ def invalid? object
16
+ !valid? object
17
+ end
18
+
19
+ def present attribute
20
+ self << Perpetuity::Validations::Presence.new(attribute)
21
+ end
22
+
23
+ def length attribute, options = {}
24
+ self << Perpetuity::Validations::Length.new(attribute, options)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1 @@
1
+ require 'perpetuity/validations/validation_set'
@@ -0,0 +1,3 @@
1
+ module Perpetuity
2
+ VERSION = "0.1"
3
+ end
data/lib/perpetuity.rb ADDED
@@ -0,0 +1,23 @@
1
+ require "perpetuity/version"
2
+ require "perpetuity/retrieval"
3
+ require "perpetuity/mongodb"
4
+ require "perpetuity/config"
5
+ require "perpetuity/mapper"
6
+
7
+ module Perpetuity
8
+ def self.configure &block
9
+ configuration.instance_exec(&block)
10
+ end
11
+
12
+ def self.configuration
13
+ @@configuration ||= Configuration.new
14
+ end
15
+
16
+ def self.generate_mapper_for klass, &block
17
+ Mapper.generate_for klass, &block
18
+ end
19
+
20
+ def self.[] klass
21
+ Mapper[klass]
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "perpetuity/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "perpetuity"
7
+ s.version = Perpetuity::VERSION
8
+ s.authors = ["Jamie Gaskins"]
9
+ s.email = ["jgaskins@gmail.com"]
10
+ s.homepage = "https://github.com/jgaskins/perpetuity.git"
11
+ s.summary = %q{Persistence library allowing persistence of Ruby objects}
12
+ s.description = %q{Persistence library allowing persistence of Ruby objects without adding persistence concerns to domain objects.}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ # specify any dependencies here; for example:
20
+ s.add_development_dependency "rake"
21
+ s.add_development_dependency "rspec", "~> 2.8.0"
22
+ s.add_development_dependency "guard-rspec"
23
+ s.add_runtime_dependency "mongo"
24
+ s.add_runtime_dependency "bson_ext"
25
+ end
@@ -0,0 +1,19 @@
1
+ require 'perpetuity/attribute_set'
2
+
3
+ module Perpetuity
4
+ describe AttributeSet do
5
+ it 'contains attributes' do
6
+ attribute = double('Attribute')
7
+ subject << attribute
8
+
9
+ subject.first.should eq attribute
10
+ end
11
+
12
+ it 'can access attributes by name' do
13
+ user_attribute = double('Attribute', name: :user)
14
+ subject << user_attribute
15
+
16
+ subject[:user].should eq user_attribute
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'perpetuity/attribute'
2
+
3
+ module Perpetuity
4
+ describe Attribute do
5
+ subject { Attribute.new :article, Object }
6
+ it 'has a name' do
7
+ subject.name.should == :article
8
+ end
9
+
10
+ it 'has a type' do
11
+ subject.type.should == Object
12
+ end
13
+
14
+ it 'can be embedded' do
15
+ attribute = Attribute.new :article, Object, embedded: true
16
+ attribute.should be_embedded
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ require 'perpetuity/config'
2
+
3
+ module Perpetuity
4
+ describe Configuration do
5
+ let(:config) { Configuration.new }
6
+
7
+ it 'sets a data source' do
8
+ db = double('db')
9
+ config.data_source db
10
+ config.data_source.should == db
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ require 'perpetuity/data_injectable'
2
+
3
+ module Perpetuity
4
+ describe DataInjectable do
5
+ let(:klass) { Class.new }
6
+ let(:object) { klass.new }
7
+
8
+ before { klass.extend DataInjectable }
9
+
10
+ it 'injects data into an object' do
11
+ klass.inject_data object, { a: 1, b: 2 }
12
+ object.instance_variable_get(:@a).should eq 1
13
+ object.instance_variable_get(:@b).should eq 2
14
+ end
15
+
16
+ it 'injects an id' do
17
+ klass.inject_data object, { id: 1 }
18
+ object.id.should eq 1
19
+ end
20
+
21
+ it 'injects a specified id' do
22
+ klass.give_id_to object, 2
23
+ object.id.should eq 2
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ require 'perpetuity/mapper'
2
+
3
+ module Perpetuity
4
+ describe Mapper do
5
+ let(:mapper) do
6
+ Mapper.new do
7
+ end
8
+ end
9
+
10
+ subject { mapper }
11
+
12
+ it { should be_a Mapper }
13
+
14
+ it 'has correct attributes' do
15
+ Mapper.new { attribute :name, String }.attributes.should eq [:name]
16
+ end
17
+
18
+ it 'returns an empty attribute list when no attributes have been assigned' do
19
+ Mapper.new.attributes.should be_empty
20
+ end
21
+
22
+ it 'can have embedded attributes' do
23
+ mapper_with_embedded_attrs = Mapper.new { attribute :comments, Array, embedded: true }
24
+ mapper_with_embedded_attrs.attribute_set[:comments].should be_embedded
25
+ end
26
+
27
+ its(:mapped_class) { should eq Object }
28
+
29
+ context 'with unserializable attributes' do
30
+ let(:unserializable_object) { 1.to_c }
31
+ let(:serialized_attrs) do
32
+ [ Marshal.dump(unserializable_object) ]
33
+ end
34
+
35
+ it 'serializes attributes' do
36
+ object = Object.new
37
+ object.stub(sub_objects: [unserializable_object])
38
+ mapper.attribute :sub_objects, Array, embedded: true
39
+ mapper.attributes_for(object)[:sub_objects].should eq serialized_attrs
40
+ end
41
+
42
+ describe 'unserializes attributes' do
43
+ let(:comments) { mapper.unserialize(serialized_attrs) }
44
+ subject { comments.first }
45
+
46
+ it { should be_a Complex }
47
+ it { should eq unserializable_object }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,42 @@
1
+ require 'perpetuity/mongodb/query_attribute'
2
+
3
+ module Perpetuity
4
+ describe MongoDB::QueryAttribute do
5
+ let(:attribute) { MongoDB::QueryAttribute.new :attribute_name }
6
+ subject { attribute }
7
+
8
+ its(:name) { should == :attribute_name }
9
+
10
+ it 'checks for equality' do
11
+ (attribute == 1).should be_a MongoDB::QueryExpression
12
+ end
13
+
14
+ it 'checks for less than' do
15
+ (attribute < 1).should be_a MongoDB::QueryExpression
16
+ end
17
+
18
+ it 'checks for <=' do
19
+ (attribute <= 1).should be_a MongoDB::QueryExpression
20
+ end
21
+
22
+ it 'checks for greater than' do
23
+ (attribute > 1).should be_a MongoDB::QueryExpression
24
+ end
25
+
26
+ it 'checks for >=' do
27
+ (attribute >= 1).should be_a MongoDB::QueryExpression
28
+ end
29
+
30
+ it 'checks for inequality' do
31
+ attribute.not_equal?(1).should be_a MongoDB::QueryExpression
32
+ end
33
+
34
+ it 'checks for regexp matches' do
35
+ (attribute =~ /value/).should be_a MongoDB::QueryExpression
36
+ end
37
+
38
+ it 'checks for inclusion' do
39
+ (attribute.in [1, 2, 3]).should be_a MongoDB::QueryExpression
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,49 @@
1
+ require 'perpetuity/mongodb/query_expression'
2
+
3
+ module Perpetuity
4
+ describe MongoDB::QueryExpression do
5
+ let(:expression) { MongoDB::QueryExpression.new :attribute, :equals, :value }
6
+ subject { expression }
7
+
8
+ describe 'translation to Mongo expressions' do
9
+ it 'equality expression' do
10
+ expression.to_db.should == { attribute: :value }
11
+ end
12
+
13
+ it 'less-than expression' do
14
+ expression.comparator = :less_than
15
+ expression.to_db.should == { attribute: { '$lt' => :value } }
16
+ end
17
+
18
+ it 'less-than-or-equal-to expression' do
19
+ expression.comparator = :lte
20
+ expression.to_db.should == { attribute: { '$lte' => :value } }
21
+ end
22
+
23
+ it 'greater-than expression' do
24
+ expression.comparator = :greater_than
25
+ expression.to_db.should == { attribute: { '$gt' => :value } }
26
+ end
27
+
28
+ it 'greater-than-or-equal-to expression' do
29
+ expression.comparator = :gte
30
+ expression.to_db.should == { attribute: { '$gte' => :value } }
31
+ end
32
+
33
+ it 'not-equal' do
34
+ expression.comparator = :not_equal
35
+ expression.to_db.should == { attribute: { '$ne' => :value } }
36
+ end
37
+
38
+ it 'checks for inclusion' do
39
+ expression.comparator = :in
40
+ expression.to_db.should == { attribute: { '$in' => :value } }
41
+ end
42
+
43
+ it 'checks for regexp matching' do
44
+ expression.comparator = :matches
45
+ expression.to_db.should == { attribute: :value }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,37 @@
1
+ require 'perpetuity/mongodb/query'
2
+
3
+ module Perpetuity
4
+ describe MongoDB::Query do
5
+ let(:query) { MongoDB::Query }
6
+
7
+ it 'generates Mongo equality expressions' do
8
+ query.new{name == 'Jamie'}.to_db.should == {name: 'Jamie'}
9
+ end
10
+
11
+ it 'generates Mongo less-than expressions' do
12
+ query.new{quantity < 10}.to_db.should == {quantity: { '$lt' => 10}}
13
+ end
14
+
15
+ it 'generates Mongo less-than-or-equal expressions' do
16
+ query.new{quantity <= 10}.to_db.should == {quantity: { '$lte' => 10}}
17
+ end
18
+
19
+ it 'generates Mongo greater-than expressions' do
20
+ query.new{quantity > 10}.to_db.should == {quantity: { '$gt' => 10}}
21
+ end
22
+
23
+ it 'generates Mongo greater-than-or-equal expressions' do
24
+ query.new{quantity >= 10}.to_db.should == {quantity: { '$gte' => 10}}
25
+ end
26
+
27
+ it 'generates Mongo inequality expressions' do
28
+ query.new{name.not_equal? 'Jamie'}.to_db.should == {
29
+ name: {'$ne' => 'Jamie'}
30
+ }
31
+ end
32
+
33
+ it 'generates Mongo regexp expressions' do
34
+ query.new{name =~ /Jamie/}.to_db.should == {name: /Jamie/}
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,91 @@
1
+ require 'perpetuity/mongodb'
2
+
3
+ module Perpetuity
4
+ describe MongoDB do
5
+ let(:mongo) { MongoDB.new db: 'perpetuity_gem_test' }
6
+ let(:klass) { String }
7
+ subject { mongo }
8
+
9
+ it 'is not connected when instantiated' do
10
+ mongo.should_not be_connected
11
+ end
12
+
13
+ it 'connects to its host' do
14
+ connection = double('connection')
15
+ Mongo::Connection.stub(new: connection)
16
+ mongo.connect
17
+ mongo.should be_connected
18
+ mongo.connection.should == connection
19
+ end
20
+
21
+ it 'connects automatically when accessing the database' do
22
+ mongo.database
23
+ mongo.should be_connected
24
+ end
25
+
26
+ describe 'initialization params' do
27
+ let(:host) { double('host') }
28
+ let(:port) { double('port') }
29
+ let(:db) { double('db') }
30
+ let(:pool_size) { double('pool size') }
31
+ let(:username) { double('username') }
32
+ let(:password) { double('password') }
33
+ let(:mongo) do
34
+ MongoDB.new(
35
+ host: host,
36
+ port: port,
37
+ db: db,
38
+ pool_size: pool_size,
39
+ username: username,
40
+ password: password
41
+ )
42
+ end
43
+ subject { mongo }
44
+
45
+ its(:host) { should == host }
46
+ its(:port) { should == port }
47
+ its(:db) { should == db }
48
+ its(:pool_size) { should == pool_size }
49
+ its(:username) { should == username }
50
+ its(:password) { should == password }
51
+ end
52
+
53
+ it 'uses the selected database' do
54
+ mongo.database.name.should == 'perpetuity_gem_test'
55
+ end
56
+
57
+ it 'removes all documents from a collection' do
58
+ mongo.insert klass, {}
59
+ mongo.delete_all klass
60
+ mongo.count(klass).should == 0
61
+ end
62
+
63
+ it 'counts the documents in a collection' do
64
+ mongo.delete_all klass
65
+ 3.times do
66
+ mongo.insert klass, {}
67
+ end
68
+ mongo.count(klass).should == 3
69
+ end
70
+
71
+ it 'gets the first document in a collection' do
72
+ value = {value: 1}
73
+ mongo.insert klass, value
74
+ mongo.first(klass)[:hypothetical_value].should == value['value']
75
+ end
76
+
77
+ it 'gets all of the documents in a collection' do
78
+ values = [{value: 1}, {value: 2}]
79
+ mongo.should_receive(:retrieve).with(Object, {}, {})
80
+ .and_return(values)
81
+ mongo.all(Object).should == values
82
+ end
83
+
84
+ it 'retrieves by id if the id is a string' do
85
+ time = Time.now.utc
86
+ id = mongo.insert Article, {inserted: time}
87
+ objects = mongo.retrieve(Article, id: id.to_s).to_a
88
+ objects.map{|i| i["inserted"].to_f}.first.should be_within(0.001).of time.to_f
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,58 @@
1
+ require 'perpetuity/retrieval'
2
+
3
+ module Perpetuity
4
+ describe Retrieval do
5
+ let(:data_source) { double('data_source') }
6
+ let(:retrieval) { Perpetuity::Retrieval.new Object, {}, data_source }
7
+ subject { retrieval }
8
+
9
+ it "sorts the results" do
10
+ sorted = retrieval.sort(:name)
11
+ sorted.sort_attribute.should == :name
12
+ end
13
+
14
+ it "reverses the sort order of the results" do
15
+ sorted = retrieval.sort(:name).reverse
16
+ sorted.sort_direction.should == :descending
17
+ end
18
+
19
+ it "limits the result set" do
20
+ retrieval.limit(1).result_limit.should == 1
21
+ end
22
+
23
+ it 'indicates whether it includes a specific item' do
24
+ subject.stub(to_a: [1])
25
+ subject.should include 1
26
+ end
27
+
28
+ it 'can be empty' do
29
+ retrieval.stub(to_a: [])
30
+ retrieval.should be_empty
31
+ end
32
+
33
+ describe 'pagination' do
34
+ let(:paginated) { retrieval.page(2) }
35
+ it 'paginates data' do
36
+ paginated.result_page.should == 2
37
+ end
38
+
39
+ it 'defaults to 20 items per page' do
40
+ paginated.quantity_per_page.should == 20
41
+ end
42
+
43
+ it 'sets the number of items per page' do
44
+ paginated.per_page(50).quantity_per_page.should == 50
45
+ end
46
+ end
47
+
48
+ it 'retrieves data from the data source' do
49
+ return_data = { id: 0, a: 1, b: 2 }
50
+ options = { attribute: nil, direction: nil, limit: nil, page: nil }
51
+ data_source.should_receive(:retrieve).with(Object, {}, options).
52
+ and_return([return_data])
53
+ results = retrieval.to_a
54
+
55
+ results.map(&:id).should == [0]
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,53 @@
1
+ require 'perpetuity/validations/length'
2
+
3
+ module Perpetuity
4
+ module Validations
5
+ describe Length do
6
+ let(:length) { Length.new :to_s, {}}
7
+
8
+ describe 'minimum length' do
9
+ before { length.at_least 4 }
10
+
11
+ it 'invalidates' do
12
+ length.pass?('abc').should be false
13
+ end
14
+
15
+ it 'validates' do
16
+ length.pass?('abcd').should be true
17
+ end
18
+ end
19
+
20
+ describe 'maximum length' do
21
+ before { length.at_most 4 }
22
+
23
+ it 'validates' do
24
+ length.pass?('abcd').should be true
25
+ end
26
+
27
+ it 'invalidates' do
28
+ length.pass?('abcde').should be false
29
+ end
30
+ end
31
+
32
+ describe 'ranges' do
33
+ before { length.between 4..5 }
34
+
35
+ it 'invalidates values too short' do
36
+ length.pass?('abc').should be false
37
+ end
38
+
39
+ it 'validates lengths at the low end' do
40
+ length.pass?('abcd').should be true
41
+ end
42
+
43
+ it 'validates lengths at the high end' do
44
+ length.pass?('abcde').should be true
45
+ end
46
+
47
+ it 'invalidates values too long' do
48
+ length.pass?('abcdef').should be false
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end