repository 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ *.gem
2
+ *.rbc
3
+ .DS_Store
4
+ .bundle
5
+ .rvmrc
6
+ .yardoc
7
+ Gemfile.lock
8
+ doc/*
9
+ log/*
10
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format=documentation
2
+ --color
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - jruby
6
+ - rbx
7
+ - ree
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
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.
@@ -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
+
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :test => :spec
10
+ task :default => :spec
11
+
@@ -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