perpetuity 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 (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