repository 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +76 -0
- data/Rakefile +11 -0
- data/lib/repository.rb +78 -0
- data/lib/repository/criterion.rb +192 -0
- data/lib/repository/repository.rb +126 -0
- data/lib/repository/stash.rb +59 -0
- data/lib/repository/stash_storage.rb +34 -0
- data/lib/repository/storage.rb +78 -0
- data/repository.gemspec +22 -0
- data/spec/domain.rb +21 -0
- data/spec/helper.rb +51 -0
- data/spec/lib/criterion_conjunction_spec.rb +58 -0
- data/spec/lib/criterion_contains_spec.rb +48 -0
- data/spec/lib/criterion_equals_spec.rb +79 -0
- data/spec/lib/criterion_factory_spec.rb +69 -0
- data/spec/lib/criterion_join_spec.rb +42 -0
- data/spec/lib/criterion_key_spec.rb +64 -0
- data/spec/lib/criterion_refers_to_spec.rb +12 -0
- data/spec/lib/criterion_spec.rb +120 -0
- data/spec/lib/repository_spec.rb +32 -0
- data/spec/lib/stash_spec.rb +185 -0
- metadata +135 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
# a Stash is a glorified hashtable, used for keeping objects in memory
|
2
|
+
# and indexing them by key. Any objects put into the stash must have an
|
3
|
+
# key, as identified by the Keymaster class. A stash is used by Repository
|
4
|
+
# to keep the objects it recieves from its Storage.
|
5
|
+
module Repository
|
6
|
+
class Stash
|
7
|
+
|
8
|
+
attr_reader :data # for debugging only
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@data = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def size
|
15
|
+
@data.size
|
16
|
+
end
|
17
|
+
|
18
|
+
def clear
|
19
|
+
@data = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Put an object, or an array of objects, into the stash.
|
23
|
+
# All such objects must be identifiable by the Keymaster class;
|
24
|
+
# if not, this will raise a Repository::Storage::Unidentified exception
|
25
|
+
# (possibly leaving some remaining items unstashed).
|
26
|
+
def put(object)
|
27
|
+
if object.is_a? Array
|
28
|
+
object.each do |o|
|
29
|
+
put(o)
|
30
|
+
end
|
31
|
+
else
|
32
|
+
key = object.id
|
33
|
+
raise Storage::Unidentified, "you can't stash an object without id" unless key
|
34
|
+
@data[key] = object
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# get an object by key
|
39
|
+
def get(key)
|
40
|
+
@data[key]
|
41
|
+
end
|
42
|
+
|
43
|
+
alias_method :[], :get
|
44
|
+
|
45
|
+
# Finds all stashed objects that match the argument. Argument is either
|
46
|
+
# a criterion, an key, or an array of either keys or criteria.
|
47
|
+
# Returns an array of objects.
|
48
|
+
def find(arg)
|
49
|
+
if arg.is_a? Criterion
|
50
|
+
@data.values.select{|object| arg.match? object}
|
51
|
+
elsif arg.is_a? Array
|
52
|
+
arg.map{|key| get(key)}
|
53
|
+
else
|
54
|
+
[get(arg)]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Repository
|
2
|
+
class StashStorage < Storage
|
3
|
+
|
4
|
+
attr_reader :stash
|
5
|
+
|
6
|
+
def initialize(klass, stash = Stash.new)
|
7
|
+
raise "nope" if klass.is_a? Stash
|
8
|
+
raise "nuh-uh" unless stash.is_a? Stash
|
9
|
+
super(klass)
|
10
|
+
@stash = stash
|
11
|
+
end
|
12
|
+
|
13
|
+
def size
|
14
|
+
@stash.size
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear
|
18
|
+
@stash.clear
|
19
|
+
end
|
20
|
+
|
21
|
+
def store(objects)
|
22
|
+
@stash.put(objects)
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_by_criterion(criterion)
|
26
|
+
@stash.find(criterion)
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_by_keys(keys)
|
30
|
+
@stash.find(keys)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Repository
|
2
|
+
|
3
|
+
# Storage is an abstract base class for the backing storage area of
|
4
|
+
# a Repository. Concrete implementations include Stash (for in-memory
|
5
|
+
# storage).
|
6
|
+
class Storage
|
7
|
+
|
8
|
+
class Unidentified < RuntimeError
|
9
|
+
end
|
10
|
+
|
11
|
+
class Unimplemented < RuntimeError
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.for_class(klass)
|
15
|
+
StashStorage.new(klass)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(klass = nil)
|
19
|
+
@klass = klass
|
20
|
+
end
|
21
|
+
|
22
|
+
def size
|
23
|
+
raise Unimplemented
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear
|
27
|
+
raise Unimplemented
|
28
|
+
end
|
29
|
+
|
30
|
+
# Put an object, or an array of objects, into the storage.
|
31
|
+
def store(objects)
|
32
|
+
unless objects.is_a? Array
|
33
|
+
objects = [objects]
|
34
|
+
end
|
35
|
+
objects.each do |object|
|
36
|
+
store(object)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# get an object by key
|
41
|
+
def get(key)
|
42
|
+
find_by_keys([key]).first
|
43
|
+
end
|
44
|
+
|
45
|
+
# alias for get
|
46
|
+
def [](key)
|
47
|
+
get(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Finds all stashed objects that match the argument. Argument is either
|
51
|
+
# a criterion, an key, or an array of either keys or criteria.
|
52
|
+
# Returns an array of objects.
|
53
|
+
def find(arg)
|
54
|
+
if arg.is_a? Criterion
|
55
|
+
find_by_criterion(arg)
|
56
|
+
elsif arg.is_a? Array
|
57
|
+
find_by_keys(arg)
|
58
|
+
else
|
59
|
+
find_by_keys([arg])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def store(objects)
|
66
|
+
raise Unimplemented
|
67
|
+
end
|
68
|
+
|
69
|
+
def find_by_criterion(criterion)
|
70
|
+
raise Unimplemented
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_by_keys(keys)
|
74
|
+
raise Unimplemented
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
data/repository.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.add_dependency 'active_support', [">= 0"]
|
5
|
+
gem.add_development_dependency 'pry'
|
6
|
+
gem.add_development_dependency 'rake'
|
7
|
+
gem.add_development_dependency 'rspec'
|
8
|
+
|
9
|
+
gem.authors = ["Alex Chaffee", "Nikita Fedyashev"]
|
10
|
+
gem.description = %q{A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection.}
|
11
|
+
gem.email = 'loci.master@gmail.com'
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.homepage = 'http://github.com/nfedyashev/repository'
|
14
|
+
gem.name = 'repository'
|
15
|
+
gem.require_paths = ['lib']
|
16
|
+
gem.extra_rdoc_files = ['README.md']
|
17
|
+
gem.required_rubygems_version = Gem::Requirement.new(">= 1.3.6") if gem.respond_to? :required_rubygems_version=
|
18
|
+
gem.summary = %q{A Ruby implementation of the Repository Pattern}
|
19
|
+
gem.test_files = `git ls-files -- spec/*`.split("\n")
|
20
|
+
gem.version = '0.0.1'
|
21
|
+
end
|
22
|
+
|
data/spec/domain.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Repository
|
4
|
+
|
5
|
+
class Project < OpenStruct
|
6
|
+
end
|
7
|
+
|
8
|
+
class User < OpenStruct
|
9
|
+
def initialize(hash = {})
|
10
|
+
super({:name => nil}.merge(hash))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Address < OpenStruct
|
15
|
+
end
|
16
|
+
|
17
|
+
class Country < OpenStruct
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
ENV["T_ENV"] = "test"
|
2
|
+
require 'repository'
|
3
|
+
require 'rspec'
|
4
|
+
require 'pry'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
7
|
+
|
8
|
+
require 'domain'
|
9
|
+
|
10
|
+
module CustomMatchers
|
11
|
+
# borrowed from http://github.com/aiwilliams/spec_goodies
|
12
|
+
|
13
|
+
class IncludeOnly # :nodoc:all
|
14
|
+
def initialize(*expected)
|
15
|
+
@expected = expected.flatten
|
16
|
+
end
|
17
|
+
|
18
|
+
def matches?(actual)
|
19
|
+
@missing = @expected.reject {|e| actual.include?(e)}
|
20
|
+
@extra = actual.reject {|e| @expected.include?(e)}
|
21
|
+
@extra.empty? && @missing.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def failure_message
|
25
|
+
message = "expected to include only #{@expected.inspect}"
|
26
|
+
message << "\nextra: #{@extra.inspect}" unless @extra.empty?
|
27
|
+
message << "\nmissing: #{@missing.inspect}" unless @missing.empty?
|
28
|
+
message
|
29
|
+
end
|
30
|
+
|
31
|
+
def negative_failure_message
|
32
|
+
"expected to include more than #{@expected.inspect}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"include only #{@expected.inspect}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Unlike checking that two Enumerables are equal, where the
|
41
|
+
# objects in corresponding positions must be equal, this will
|
42
|
+
# allow you to ensure that an Enumerable has all the objects
|
43
|
+
# you expect, in any order; no more, no less.
|
44
|
+
def include_only(*expected)
|
45
|
+
IncludeOnly.new(*expected)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
RSpec.configure do |config|
|
50
|
+
include CustomMatchers
|
51
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
module Repository
|
5
|
+
describe Criterion do
|
6
|
+
before do
|
7
|
+
@alice = User.new(:name => "Alice", :id => 1)
|
8
|
+
@bob = User.new(:name => "Bob", :id => 2)
|
9
|
+
@charlie = User.new(:name => "Charlie", :id => 3)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Criterion::And do
|
13
|
+
before do
|
14
|
+
@c = Criterion::And.new(
|
15
|
+
(@c1 = Criterion::Contains.new(:subject => "name", :value => "a")),
|
16
|
+
(@c2 = Criterion::Contains.new(:subject => "name", :value => "r"))
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#match' do
|
21
|
+
|
22
|
+
it "fails to match if no criteria match" do
|
23
|
+
@c.should_not be_match(@bob)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "fails to match if only one criterion matches" do
|
27
|
+
@c.should_not be_match(@alice)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "matches if all criteria match" do
|
31
|
+
@c.should be_match(@charlie)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe Criterion::Or do
|
37
|
+
before do
|
38
|
+
@c = Criterion::Or.new(
|
39
|
+
(@c1 = Criterion::Contains.new(:subject => "name", :value => "o")), # 'o' is only in 'bob'
|
40
|
+
(@c2 = Criterion::Contains.new(:subject => "name", :value => "r")) # 'r' is only in 'charlie'
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#match' do
|
45
|
+
it "fails to match if no criteria match" do
|
46
|
+
@c.should_not be_match(@alice)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "matches if only one criterion matches" do
|
50
|
+
@c.should be_match(@bob)
|
51
|
+
@c.should be_match(@charlie)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
module Repository
|
5
|
+
describe Criterion do
|
6
|
+
before do
|
7
|
+
@alice = User.new(:name => "Alice", :id => 1)
|
8
|
+
@bob = User.new(:name => "Bob", :id => 2)
|
9
|
+
@charlie = User.new(:name => "Charlie", :id => 3)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Criterion::Contains do
|
13
|
+
before do
|
14
|
+
@c = Criterion::Contains.new(:subject => "name", :value => "ABC")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "has a swell descriptor" do
|
18
|
+
@c.descriptor.should == "name contains"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "matches identical text" do
|
22
|
+
@c.should be_match(User.new(:name => "ABC"))
|
23
|
+
end
|
24
|
+
it "matches mixed-case identical text" do
|
25
|
+
@c.should be_match(User.new(:name => "AbC"))
|
26
|
+
end
|
27
|
+
it "matches text in the middle" do
|
28
|
+
@c.should be_match(User.new(:name => "drabCouch"))
|
29
|
+
end
|
30
|
+
it "doesn't match different text" do
|
31
|
+
@c.should_not be_match(User.new(:name => "abx"))
|
32
|
+
end
|
33
|
+
describe "multiple values" do
|
34
|
+
before do
|
35
|
+
@c = Criterion::Contains.new(:subject => "name", :descriptor => "is kinda named", :value => ["ABC", "123"])
|
36
|
+
end
|
37
|
+
it "matches any" do
|
38
|
+
@c.should be_match(User.new(:name => "abc"))
|
39
|
+
@c.should be_match(User.new(:name => "123"))
|
40
|
+
end
|
41
|
+
it "fails to match" do
|
42
|
+
@c.should_not be_match(User.new(:name => "12ab3c"))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
module Repository
|
5
|
+
describe Criterion do
|
6
|
+
before do
|
7
|
+
@alice = User.new(:name => "Alice", :id => 1)
|
8
|
+
@bob = User.new(:name => "Bob", :id => 2)
|
9
|
+
@charlie = User.new(:name => "Charlie", :id => 3)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Criterion::Equals do
|
13
|
+
before do
|
14
|
+
@c = Criterion::Equals.new(:subject => "name", :value => "alex")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "has a swell descriptor" do
|
18
|
+
Criterion::Equals.new({}).descriptor.should == "id equals"
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#match' do
|
22
|
+
describe "a string" do
|
23
|
+
before do
|
24
|
+
@c = Criterion::Equals.new(:subject => "name", :value => "Alice")
|
25
|
+
end
|
26
|
+
|
27
|
+
it "matches a string" do
|
28
|
+
@c.should be_match @alice
|
29
|
+
end
|
30
|
+
|
31
|
+
it "fails to match a string" do
|
32
|
+
@c.should_not be_match @bob
|
33
|
+
end
|
34
|
+
|
35
|
+
it "fails to match a string with the wrong case" do
|
36
|
+
@c.should_not be_match User.new(:name => "alice")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "an int" do
|
41
|
+
before do
|
42
|
+
@c = Criterion::Equals.new(:subject => "id", :value => 1)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "matches an int" do
|
46
|
+
@c.match?(@alice).should be_true
|
47
|
+
end
|
48
|
+
|
49
|
+
it "fails to match an int" do
|
50
|
+
@c.match?(@bob).should be_false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "a set of ints" do
|
55
|
+
before do
|
56
|
+
@c = Criterion::Equals.new(:subject => "id", :value => [1,2])
|
57
|
+
end
|
58
|
+
|
59
|
+
it "matches" do
|
60
|
+
@c.should be_match @alice
|
61
|
+
@c.should be_match @bob
|
62
|
+
end
|
63
|
+
|
64
|
+
it "fails to match an int" do
|
65
|
+
@c.should_not be_match @charlie
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "a string value in the criterion" do
|
70
|
+
it "should match an int value in the object" do
|
71
|
+
@c = Criterion::Equals.new(:subject => "id", :value => "1")
|
72
|
+
@c.should be_match @alice
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|