repository 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.
- 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
|