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
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2009 Alex Chaffee
|
2
|
+
Copyright (c) 2012 Nikita Fedyashev
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
"Software"), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
|
2
|
+
![travis-ci](http://travis-ci.org/nfedyashev/repository.png)
|
3
|
+
|
4
|
+
Description
|
5
|
+
--------
|
6
|
+
|
7
|
+
A Ruby implementation of the Repository Pattern, as described in many OO/Patterns books but notably martinfowler.com/eaaCatalog/repository.html and www.infoq.com/resource/minibooks/domain-driven-design-quickly/en/pdf/DomainDrivenDesignQuicklyOnline.pdf (p.51)
|
8
|
+
|
9
|
+
From Fowler/Hieatt/Mee: “A system with a complex domain model often benefits from a layer, such as the one provided by Data Mapper, that isolates domain objects from details of the database access code. In such systems it can be worthwhile to build another layer of abstraction over the mapping layer where query construction code is concentrated. This becomes more important when there are a large number of domain classes or heavy querying. In these cases particularly, adding this layer helps minimize duplicate query logic. A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection.”
|
10
|
+
|
11
|
+
In Repository, queries are specified using a DSL and turn into Criterion objects, which are then passed to the Repository for matching against the data store.
|
12
|
+
|
13
|
+
Examples
|
14
|
+
--------
|
15
|
+
|
16
|
+
``` ruby
|
17
|
+
require 'ostruct'
|
18
|
+
=> true
|
19
|
+
|
20
|
+
class User < OpenStruct
|
21
|
+
end
|
22
|
+
=> nil
|
23
|
+
|
24
|
+
user = User.new(id: 1, name: 'John')
|
25
|
+
=> #<User id=1, name="John">
|
26
|
+
|
27
|
+
Repository[User].store(user)
|
28
|
+
=> [#<User id=1, name="John">]
|
29
|
+
|
30
|
+
Repository[User].search(user.id)
|
31
|
+
=> [#<User id=1, name="John">]
|
32
|
+
|
33
|
+
```
|
34
|
+
|
35
|
+
Check out specs for more examples.
|
36
|
+
|
37
|
+
Installation
|
38
|
+
-------
|
39
|
+
|
40
|
+
Install the gem:
|
41
|
+
|
42
|
+
``` bash
|
43
|
+
$ gem install repository
|
44
|
+
```
|
45
|
+
|
46
|
+
Add it to your Gemfile:
|
47
|
+
|
48
|
+
``` ruby
|
49
|
+
gem 'repository'
|
50
|
+
```
|
51
|
+
|
52
|
+
Submitting a Pull Request
|
53
|
+
-------
|
54
|
+
|
55
|
+
1. Fork the project.
|
56
|
+
2. Create a topic branch.
|
57
|
+
3. Implement your feature or bug fix.
|
58
|
+
4. Add specs for your feature or bug fix.
|
59
|
+
5. Run `bundle exec rake spec`. If your changes are not 100% covered, go back to step 4.
|
60
|
+
6. Commit and push your changes.
|
61
|
+
7. Submit a pull request. Please do not include changes to the gemspec,
|
62
|
+
version, or history file. (If you want to create your own version for some
|
63
|
+
reason, please do so in a separate commit.)
|
64
|
+
|
65
|
+
|
66
|
+
Credits
|
67
|
+
-------
|
68
|
+
|
69
|
+
That is a simplified version of the https://github.com/alexch/treasury which supports only in-memory database with minimum features and overhead. If you need more features or support of other storage methods you may check out the original repo.
|
70
|
+
|
71
|
+
|
72
|
+
Copyright © 2009 Alex Chaffee
|
73
|
+
Copyright © 2012 Nikita Fedyashev
|
74
|
+
|
75
|
+
See LICENSE.txt for further details.
|
76
|
+
|
data/Rakefile
ADDED
data/lib/repository.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), '.')
|
2
|
+
|
3
|
+
require 'active_support/inflector'
|
4
|
+
require 'active_support/core_ext/object'
|
5
|
+
|
6
|
+
require 'repository/criterion'
|
7
|
+
require 'repository/repository'
|
8
|
+
require 'repository/storage'
|
9
|
+
require 'repository/stash'
|
10
|
+
require 'repository/stash_storage'
|
11
|
+
|
12
|
+
module Repository
|
13
|
+
|
14
|
+
def self.repositories
|
15
|
+
(@@repositories ||= {})
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.repository(klass)
|
19
|
+
self.repositories[klass] ||= Repository.new(klass)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.[](klass)
|
23
|
+
self.repository(klass)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.clear_all
|
27
|
+
self.repositories.values.each do |r|
|
28
|
+
r.clear
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.[]=(klass, repository)
|
33
|
+
self.repositories[klass] = repository
|
34
|
+
end
|
35
|
+
|
36
|
+
# methods on Repository-enabled model classes that extend Repository
|
37
|
+
|
38
|
+
def repository
|
39
|
+
Repository[self]
|
40
|
+
end
|
41
|
+
|
42
|
+
def repository_size
|
43
|
+
repository.size
|
44
|
+
end
|
45
|
+
|
46
|
+
def store(*args)
|
47
|
+
repository.store(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def search(*args, &block)
|
51
|
+
repository.search(*args, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
def clear_repository
|
55
|
+
repository.clear
|
56
|
+
end
|
57
|
+
|
58
|
+
def <<( *treasure )
|
59
|
+
store( *treasure )
|
60
|
+
end
|
61
|
+
|
62
|
+
def [](arg)
|
63
|
+
repository[arg]
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.extended( klass )
|
67
|
+
klass.class_eval do
|
68
|
+
include InstanceMethods
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module InstanceMethods
|
73
|
+
def store
|
74
|
+
self.class.store(self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module Repository
|
2
|
+
class Criterion
|
3
|
+
|
4
|
+
attr_reader :subject, :descriptor, :value, :property_name
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@subject = (options[:subject] || "id").to_s
|
8
|
+
@descriptor = options[:descriptor] || "#{@subject} #{default_descriptor}"
|
9
|
+
@value = options[:value]
|
10
|
+
@value = nil if @value.blank?
|
11
|
+
@property_name = options[:property_name] || @subject
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
self.class == other.class &&
|
16
|
+
self.subject == other.subject &&
|
17
|
+
self.descriptor == other.descriptor &&
|
18
|
+
self.value == other.value &&
|
19
|
+
self.property_name == other.property_name
|
20
|
+
end
|
21
|
+
|
22
|
+
def description
|
23
|
+
"#{descriptor} #{described_value}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_descriptor
|
27
|
+
self.class.name.gsub(/[A-Z]/, " \\0").gsub(/.*::/, '').downcase.strip
|
28
|
+
end
|
29
|
+
|
30
|
+
def described_value
|
31
|
+
if value.blank?
|
32
|
+
"any"
|
33
|
+
elsif value == 0
|
34
|
+
"none"
|
35
|
+
else
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def match?(object)
|
41
|
+
object_value = object.send(property_name)
|
42
|
+
if value.is_a? Array
|
43
|
+
value.detect{|criterion_value| match_value?(criterion_value, object_value)}
|
44
|
+
else
|
45
|
+
match_value?(value, object_value)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def +(other)
|
50
|
+
And.new(self, other)
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method :&, :+
|
54
|
+
|
55
|
+
def |(other)
|
56
|
+
Or.new(self, other)
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_in(storage)
|
60
|
+
storage.find(self)
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
def match_value?(criterion_value, object_value)
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
68
|
+
public
|
69
|
+
|
70
|
+
class Factory
|
71
|
+
# methods are defined inside the relevant Criterion subclasses
|
72
|
+
end
|
73
|
+
|
74
|
+
class Equals < Criterion
|
75
|
+
def Factory.equals(subject, value)
|
76
|
+
Equals.new(:subject => subject, :value => value)
|
77
|
+
end
|
78
|
+
|
79
|
+
def match_value?(criterion_value, object_value)
|
80
|
+
if object_value.is_a? Fixnum
|
81
|
+
object_value == criterion_value.to_i
|
82
|
+
else
|
83
|
+
object_value == criterion_value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def value
|
88
|
+
case @value
|
89
|
+
when Criterion
|
90
|
+
@value.value
|
91
|
+
else
|
92
|
+
@value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Key < Equals
|
98
|
+
def Factory.key(*args)
|
99
|
+
if args.length == 1
|
100
|
+
subject = :key
|
101
|
+
value = args.first
|
102
|
+
else
|
103
|
+
subject = args.shift
|
104
|
+
value = args.shift
|
105
|
+
end
|
106
|
+
Key.new(:subject => subject, :value => value)
|
107
|
+
end
|
108
|
+
|
109
|
+
def initialize(options)
|
110
|
+
options[:value] = case options[:value]
|
111
|
+
when Fixnum
|
112
|
+
[options[:value]]
|
113
|
+
when String
|
114
|
+
[options[:value].to_i]
|
115
|
+
when Array
|
116
|
+
options[:value].map{|v|v.to_i} # todo: allow arrays of Criteria too
|
117
|
+
else
|
118
|
+
options[:value]
|
119
|
+
end
|
120
|
+
options[:descriptor] ||= "#"
|
121
|
+
super(options)
|
122
|
+
end
|
123
|
+
|
124
|
+
def match?(object)
|
125
|
+
object_value = object.send(property_name)
|
126
|
+
value.detect{|criterion_value| match_value?(criterion_value, object_value)} || false
|
127
|
+
end
|
128
|
+
|
129
|
+
def match_value?(criterion_value, object_value)
|
130
|
+
object_value == criterion_value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class Contains < Criterion
|
135
|
+
def Factory.contains(subject, value)
|
136
|
+
Contains.new(:subject => subject, :value => value)
|
137
|
+
end
|
138
|
+
|
139
|
+
protected
|
140
|
+
def match_value?(criterion_value, object_value)
|
141
|
+
/#{Regexp.escape(criterion_value)}/i =~ object_value
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class Conjunction < Criterion
|
146
|
+
attr_reader :criteria
|
147
|
+
def initialize(*criteria)
|
148
|
+
super(:subject => nil)
|
149
|
+
@criteria = criteria
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class And < Conjunction
|
154
|
+
def match?(object)
|
155
|
+
@criteria.each do |criterion|
|
156
|
+
return false unless criterion.match?(object)
|
157
|
+
end
|
158
|
+
return true
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class Or < Conjunction
|
163
|
+
def match?(object)
|
164
|
+
@criteria.each do |criterion|
|
165
|
+
return true if criterion.match?(object)
|
166
|
+
end
|
167
|
+
return false
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
class Join < Criterion
|
172
|
+
attr_reader :referent_class
|
173
|
+
|
174
|
+
def initialize(options)
|
175
|
+
super
|
176
|
+
@nested_criterion = options[:criterion]
|
177
|
+
@referent_class = options[:referent_class]
|
178
|
+
end
|
179
|
+
|
180
|
+
def find_in(storage)
|
181
|
+
objects = storage.find(@nested_criterion)
|
182
|
+
objects.map(&:id)
|
183
|
+
end
|
184
|
+
|
185
|
+
def value
|
186
|
+
@value ||= begin
|
187
|
+
Repository[@referent_class].search(self)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# http://martinfowler.com/eaaCatalog/repository.html
|
2
|
+
# http://www.infoq.com/resource/minibooks/domain-driven-design-quickly/en/pdf/DomainDrivenDesignQuicklyOnline.pdf p.51
|
3
|
+
module Repository
|
4
|
+
class Repository
|
5
|
+
|
6
|
+
attr_reader :klass
|
7
|
+
attr_accessor :storage
|
8
|
+
|
9
|
+
def initialize(klass)
|
10
|
+
@klass = klass
|
11
|
+
@stash = Stash.new
|
12
|
+
@storage = StashStorage.new(klass)
|
13
|
+
end
|
14
|
+
|
15
|
+
def size
|
16
|
+
@stash.size
|
17
|
+
end
|
18
|
+
|
19
|
+
# todo: clean up ambiguity between clearing the stash and clearing the storage
|
20
|
+
def clear
|
21
|
+
@stash.clear
|
22
|
+
end
|
23
|
+
|
24
|
+
def <<(args)
|
25
|
+
store(args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def store(*arg)
|
29
|
+
arg.flatten!
|
30
|
+
arg = arg.select do |object|
|
31
|
+
if object.is_a?(Fixnum)
|
32
|
+
false
|
33
|
+
elsif !object.is_a?(klass)
|
34
|
+
raise ArgumentError, "expected #{@klass} but got #{object.class}"
|
35
|
+
else
|
36
|
+
true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
storage.store(arg)
|
40
|
+
@stash.put(arg)
|
41
|
+
end
|
42
|
+
|
43
|
+
def [](key)
|
44
|
+
objects = search(key)
|
45
|
+
if objects.empty?
|
46
|
+
nil
|
47
|
+
else
|
48
|
+
objects.first
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def search(arg = nil, &block)
|
53
|
+
if (arg.nil? && !block_given?) || (block_given? && !arg.nil?)
|
54
|
+
raise "Must pass either an argument or a block to Repository#search"
|
55
|
+
end
|
56
|
+
|
57
|
+
arg = criterion_from &block if block_given?
|
58
|
+
|
59
|
+
case arg
|
60
|
+
when Array
|
61
|
+
find_keys(arg)
|
62
|
+
when Fixnum
|
63
|
+
find_keys([arg])
|
64
|
+
when Criterion::Key
|
65
|
+
find_keys_in_criterion(arg)
|
66
|
+
when String
|
67
|
+
find_keys([arg.to_i])
|
68
|
+
when Criterion
|
69
|
+
find_by_criterion(arg)
|
70
|
+
else
|
71
|
+
raise "???"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def extract(arg = nil, &block)
|
76
|
+
search(arg, &block).map{ |o| o.id }
|
77
|
+
end
|
78
|
+
|
79
|
+
# This is the method that's called when you pass a block into search.
|
80
|
+
# It passes the Criterion::Factory as a block parameter.
|
81
|
+
def criterion_from
|
82
|
+
yield Criterion::Factory
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def find_keys_in_criterion(criterion)
|
88
|
+
find_keys(criterion.value)
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_keys(keys)
|
92
|
+
raise "Nil argument" if keys.nil?
|
93
|
+
|
94
|
+
found = []
|
95
|
+
needed = []
|
96
|
+
keys.each do |key|
|
97
|
+
raise ArgumentError, "illegal argument #{key.inspect}" if key.to_i == 0
|
98
|
+
key = key.to_i
|
99
|
+
unless (object = @stash[key])
|
100
|
+
needed << key
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
unless needed.empty?
|
105
|
+
needed.sort!.uniq! # the sort is just to make debugging easier
|
106
|
+
|
107
|
+
found_in_storage = storage.find(needed).compact
|
108
|
+
|
109
|
+
if found_in_storage.size != needed.size
|
110
|
+
missing = (needed - found_in_storage.map(&:key))
|
111
|
+
#raise "Warning: couldn't find #{missing.size} out of #{needed.size} #{klass.name.pluralize}: missing keys #{missing.join(',')}"
|
112
|
+
end
|
113
|
+
|
114
|
+
store(found_in_storage)
|
115
|
+
end
|
116
|
+
keys.map{|key| @stash[key]} # find again so they come back in order
|
117
|
+
end
|
118
|
+
|
119
|
+
def find_by_criterion(criterion)
|
120
|
+
results = criterion.find_in(storage)
|
121
|
+
self << results
|
122
|
+
results
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|