gitmodel 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -5,3 +5,4 @@
5
5
  coverage
6
6
  pkg
7
7
  rdoc
8
+ *.swp
data/Gemfile.lock CHANGED
@@ -6,20 +6,19 @@ PATH
6
6
  activesupport (>= 3.0.1)
7
7
  grit (>= 2.3.0)
8
8
  lockfile (>= 1.4.3)
9
+ yajl-ruby (>= 0.8.2)
9
10
 
10
11
  GEM
11
12
  remote: http://rubygems.org/
12
13
  specs:
13
14
  ZenTest (4.5.0)
14
- activemodel (3.0.4)
15
- activesupport (= 3.0.4)
15
+ activemodel (3.0.9)
16
+ activesupport (= 3.0.9)
16
17
  builder (~> 2.1.2)
17
- i18n (~> 0.4)
18
- activesupport (3.0.4)
18
+ i18n (~> 0.5.0)
19
+ activesupport (3.0.9)
19
20
  autotest (4.4.6)
20
21
  ZenTest (>= 4.4.1)
21
- autotest-fsevent (0.2.4)
22
- sys-uname
23
22
  builder (2.1.2)
24
23
  diff-lcs (1.1.2)
25
24
  grit (2.4.1)
@@ -28,15 +27,15 @@ GEM
28
27
  i18n (0.5.0)
29
28
  lockfile (1.4.3)
30
29
  mime-types (1.16)
31
- rspec (2.5.0)
32
- rspec-core (~> 2.5.0)
33
- rspec-expectations (~> 2.5.0)
34
- rspec-mocks (~> 2.5.0)
35
- rspec-core (2.5.1)
36
- rspec-expectations (2.5.0)
30
+ rspec (2.6.0)
31
+ rspec-core (~> 2.6.0)
32
+ rspec-expectations (~> 2.6.0)
33
+ rspec-mocks (~> 2.6.0)
34
+ rspec-core (2.6.4)
35
+ rspec-expectations (2.6.0)
37
36
  diff-lcs (~> 1.1.2)
38
- rspec-mocks (2.5.0)
39
- sys-uname (0.8.5)
37
+ rspec-mocks (2.6.0)
38
+ yajl-ruby (0.8.2)
40
39
 
41
40
  PLATFORMS
42
41
  ruby
@@ -44,6 +43,5 @@ PLATFORMS
44
43
  DEPENDENCIES
45
44
  ZenTest (>= 4.4.0)
46
45
  autotest (>= 4.4.1)
47
- autotest-fsevent (>= 0.2.3)
48
46
  gitmodel!
49
47
  rspec (>= 2.0.1)
data/README.md CHANGED
@@ -17,13 +17,6 @@ machines, manipulated with standard Git client tools, can be branched and
17
17
  merged, and of course keeps the history of all changes.
18
18
 
19
19
 
20
- Status
21
- ------
22
-
23
- _It is nowhere near production ready but I'm working on it. Please feel free to
24
- contribute tests and/or code to help!_
25
-
26
-
27
20
  Why it's awesome
28
21
  ----------------
29
22
 
@@ -38,6 +31,7 @@ Why it's awesome
38
31
  * Experiment on production data using branches, for example to test a
39
32
  migration
40
33
  * Distributed (synced using standard Git push/pull)
34
+ * All ActiveModel
41
35
  * Transactions
42
36
  * Metadata for all database changes (Git commit messages, date & time, etc.)
43
37
  * In order to be easily human-editable, the database is simply files and
@@ -48,6 +42,22 @@ Why it's awesome
48
42
  * Clean and easy-to-use API
49
43
 
50
44
 
45
+ Status
46
+ ------
47
+
48
+ _It is not yet production ready but I'm working on it. Please feel free to
49
+ contribute tests and/or code to help!_
50
+
51
+ See the "To do" section below for details, but the main thing that needs
52
+ finishing is support for querying. Right now you can find an instance by it's
53
+ id, but there is incomplete support (90% complete) for querying, e.g.:
54
+
55
+ Post.find(:category => 'ruby', :date => lambda{|d| d > 1.month.ago} :order_by => :date, :order => :asc, :limit => 5)
56
+
57
+ This includes support for indexing all attributes so that queries don't need to
58
+ load every object.
59
+
60
+
51
61
  Installation
52
62
  ------------
53
63
 
@@ -77,6 +87,8 @@ Usage
77
87
  p1.image = some_binary_data
78
88
  p1.save!
79
89
 
90
+ p = Post.find('lessons-learned')
91
+
80
92
  p2 = Post.new(:id => 'hotdog-eating-contest', :title => 'I won!')
81
93
  p2.body = 'This weekend I won a hotdog eating contest!'
82
94
  p2.image = some_binary_data
@@ -96,6 +108,7 @@ Usage
96
108
  c1 = Comment.create!(:id => '2010-01-03-328', :text => '...')
97
109
  c2 = Comment.create!(:id => '2010-05-29-742', :text => '...')
98
110
 
111
+
99
112
  An example of a project that uses GitModel is [Balisong](https://github.com/pauldowman/balisong), a blogging app for coders (but it doesn't save objects to the data store. It's read-only so far, assuming that posts will be edited with a text editor).
100
113
 
101
114
 
@@ -116,42 +129,64 @@ For example, the database for the example above would have a directory
116
129
  structure that looks like this:
117
130
 
118
131
  * db-root
119
- * comments
120
- * 2010-01-03-328
121
- * _attributes.json_
122
- * 2010-05-29-742
123
- * _attributes.json_
124
- * posts
125
- * hotdog-eating-contest
126
- * _attributes.json_
127
- * _hotdogs.jpg_
128
- * _image_
129
- * _the-aftermath.jpg_
130
- * lessons-learned
131
- * _attributes.json_
132
- * _image_
133
- * running-with-scissors
134
- * _attributes.json_
135
-
136
- Contributors
132
+ * comments
133
+ * 2010-01-03-328
134
+ * _attributes.json_
135
+ * 2010-05-29-742
136
+ * _attributes.json_
137
+ * posts
138
+ * hotdog-eating-contest
139
+ * _attributes.json_
140
+ * _hotdogs.jpg_
141
+ * _image_
142
+ * _the-aftermath.jpg_
143
+ * lessons-learned
144
+ * _attributes.json_
145
+ * _image_
146
+ * running-with-scissors
147
+ * _attributes.json_
148
+
149
+
150
+ Contributing
137
151
  ------------
138
152
 
139
- * [Paul Dowman](http://pauldowman.com/about) ([@pauldowman](http://twitter.com/pauldowman))
153
+ Do you have an improvement to make? Please submit a pull request on GitHub or a
154
+ patch, including a test written with RSpec. To run all tests simply run
155
+ `autospec`.
156
+
157
+ The main author is [Paul Dowman](http://pauldowman.com/about) ([@pauldowman](http://twitter.com/pauldowman)).
140
158
 
159
+ Thanks to everyone who has contributed so far:
141
160
 
142
- To Do
161
+ * [Alex Bartlow](https://github.com/alexbartlow)
162
+
163
+
164
+ To do
143
165
  -----
144
166
 
167
+ * Finish Query support
168
+ * Update index (efficiently) when Persistable objects are saved
169
+ * Add Rake task to generate index
170
+ * Update README
145
171
  * Add validations and other feature examples to sample code in README
146
- * Querying
147
- * Use AREL?
148
172
  * Finish some pending specs
149
- * Associations
150
173
  * API documentation
151
174
  * Rails integration
152
- * rake tasks
153
- * generators
175
+ * Generators
176
+ * Rake tasks
154
177
  * Performance
155
- * Haven't optimized for performance yet.
178
+ * Haven't optimized for performance yet.
179
+ * Use [Rugged](https://github.com/libgit2/rugged) instead of Grit
180
+ * Remove the transaction lock (see transaction.rb line 19)
181
+ * Ability to iterate over result set without eager loading of all instances
182
+ * Persistable.find/find_all/etc could be based on staged files so that queries reflect uncommitted changes
183
+ * Better query support
184
+ * Associations
185
+ * Use AREL?
186
+
187
+
188
+ Bugs
189
+ ------------
156
190
 
191
+ * Grit 2.4.1 has [an issue with non-ASCII characters](https://github.com/mojombo/grit/commit/696761d8047ffd038dc2828e6a1998e3f7c3b419)
157
192
 
data/gitmodel.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'gitmodel'
3
- s.version = '0.0.2'
3
+ s.version = '0.0.3'
4
4
  s.platform = Gem::Platform::RUBY
5
5
 
6
6
  s.authors = ["Paul Dowman"]
@@ -21,10 +21,10 @@ Gem::Specification.new do |s|
21
21
  s.add_dependency 'activesupport', '>= 3.0.1'
22
22
  s.add_dependency 'grit', '>= 2.3.0'
23
23
  s.add_dependency 'lockfile', '>= 1.4.3'
24
+ s.add_dependency 'yajl-ruby', '>= 0.8.2'
24
25
 
25
26
  s.add_development_dependency 'ZenTest', '>= 4.4.0'
26
27
  s.add_development_dependency 'autotest', '>= 4.4.1'
27
- s.add_development_dependency 'autotest-fsevent', '>= 0.2.3' if RUBY_PLATFORM.downcase.include?("darwin") # OS X only
28
28
  s.add_development_dependency 'rspec', '>= 2.0.1'
29
29
 
30
30
  s.files = `git ls-files`.split("\n")
@@ -8,8 +8,8 @@ module GitModel
8
8
  class RecordNotFound < GitModelError
9
9
  end
10
10
 
11
- # Raised by GitModel::Persistable.save! and GitModel::Persistable.create! methods when record cannot be
12
- # saved because record is invalid.
11
+ # Raised by GitModel::Persistable.save! and GitModel::Persistable.create!
12
+ # methods when record cannot be saved because record is invalid.
13
13
  class RecordNotSaved < GitModelError
14
14
  end
15
15
 
@@ -22,4 +22,12 @@ module GitModel
22
22
  class NullId < GitModelError
23
23
  end
24
24
 
25
+ # Raised by GitModel::Persistable.find_all when query conditions are given
26
+ # but there is no index generated
27
+ class IndexRequired < GitModelError
28
+ end
29
+
30
+ class AttributeNotIndexed < GitModelError
31
+ end
32
+
25
33
  end
@@ -0,0 +1,79 @@
1
+ module GitModel
2
+ class Index
3
+ def initialize(model_class)
4
+ @model_class = model_class
5
+ end
6
+
7
+ def generate!
8
+ GitModel.logger.debug "Generating indexes for #{@model_class}"
9
+ # TODO it sucks to load every instance here, optimize later
10
+ @indexes = {}
11
+ @model_class.find_all.each do |o|
12
+ o.attributes.each do |attr, value|
13
+ @indexes[attr] ||= {}
14
+ @indexes[attr][value] ||= SortedSet.new
15
+ @indexes[attr][value] << o.id
16
+ end
17
+ end
18
+ end
19
+
20
+ def attr_index(attr)
21
+ self.load unless @indexes
22
+ return nil unless @indexes # this is just so that we can stub self.load in tests
23
+
24
+ ret = @indexes[attr.to_s]
25
+ raise GitModel::AttributeNotIndexed.new(attr.to_s) unless ret
26
+ return ret
27
+ end
28
+
29
+ def filename
30
+ File.join(@model_class.db_subdir, '_indexes.json')
31
+ end
32
+
33
+ def generated?
34
+ (GitModel.current_tree / filename) ? true : false
35
+ end
36
+
37
+ def save(options = {})
38
+ GitModel.logger.debug "Saving indexes for #{@model_class}..."
39
+ transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
40
+ result = transaction.execute do |t|
41
+ # convert to array because JSON hash keys must be strings
42
+ data = []
43
+ @indexes.each do |attr,values|
44
+ values_and_ids = []
45
+ values.each do |value, ids|
46
+ values_and_ids << [value, ids.to_a]
47
+ end
48
+ data << [attr,values_and_ids]
49
+ end
50
+ data = Yajl::Encoder.encode(data, nil, :pretty => true)
51
+ t.index.add(filename, data)
52
+ end
53
+ end
54
+
55
+ def load
56
+ unless generated?
57
+ GitModel.logger.debug "No index generated for #{@model_class}, not loading."
58
+ return
59
+ end
60
+
61
+ GitModel.logger.debug "Loading indexes for #{@model_class}..."
62
+ @indexes = {}
63
+ blob = GitModel.current_tree / filename
64
+
65
+ data = Yajl::Parser.parse(blob.data)
66
+ data.each do |attr_and_values|
67
+ attr = attr_and_values[0]
68
+ values = {}
69
+ attr_and_values[1].each do |value_and_ids|
70
+ value = value_and_ids[0]
71
+ ids = SortedSet.new(value_and_ids[1])
72
+ values[value] = ids
73
+ end
74
+ @indexes[attr] = values
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -13,6 +13,9 @@ module GitModel
13
13
 
14
14
  define_model_callbacks :initialize, :find, :touch, :only => :after
15
15
  define_model_callbacks :save, :create, :update, :destroy
16
+
17
+ cattr_accessor :index, true
18
+ self.index = GitModel::Index.new(self)
16
19
  end
17
20
 
18
21
  base.extend(ClassMethods)
@@ -84,31 +87,32 @@ module GitModel
84
87
  # :commit_message
85
88
  # Returns false if validations failed, otherwise returns the SHA of the commit
86
89
  def save(options = {})
87
- raise GitModel::NullId unless self.id
90
+ _run_save_callbacks do
91
+ raise GitModel::NullId unless self.id
88
92
 
89
- if new_record?
90
- raise GitModel::RecordExists if self.class.exists?(self.id)
91
- else
92
- raise GitModel::RecordDoesntExist unless self.class.exists?(self.id)
93
- end
93
+ if new_record?
94
+ raise GitModel::RecordExists if self.class.exists?(self.id)
95
+ else
96
+ raise GitModel::RecordDoesntExist unless self.class.exists?(self.id)
97
+ end
94
98
 
95
- dir = File.join(self.class.db_subdir, self.id)
99
+ GitModel.logger.debug "Saving #{self.class.name} with id: #{id}"
96
100
 
97
- transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
98
- result = transaction.execute do |t|
99
- # Write the attributes to the attributes file
100
- # NOTE: using the redundant attributes.to_hash to work around a bug in
101
- # active_support 3.0.4, remove when
102
- # JSON.generate(HashWithIndifferentAccess.new) no longer fails.
103
- t.index.add(File.join(dir, 'attributes.json'), JSON.pretty_generate(attributes.to_hash))
101
+ dir = File.join(self.class.db_subdir, self.id)
102
+
103
+ transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
104
+ result = transaction.execute do |t|
105
+ # Write the attributes to the attributes file
106
+ t.index.add(File.join(dir, 'attributes.json'), Yajl::Encoder.encode(attributes, nil, :pretty => true))
104
107
 
105
- # Write the blob files
106
- blobs.each do |name, data|
107
- t.index.add(File.join(dir, name), data)
108
+ # Write the blob files
109
+ blobs.each do |name, data|
110
+ t.index.add(File.join(dir, name), data)
111
+ end
108
112
  end
109
- end
110
113
 
111
- return result
114
+ result
115
+ end
112
116
  end
113
117
 
114
118
  # Same as #save but raises an exception on error
@@ -139,11 +143,13 @@ module GitModel
139
143
  self.id = File.basename(dir)
140
144
  @new_record = false
141
145
 
146
+ GitModel.logger.debug "Loading #{self.class.name} with id: #{id}"
147
+
142
148
  # load the attributes
143
149
  object = GitModel.current_tree / File.join(dir, 'attributes.json')
144
150
  raise GitModel::RecordNotFound if object.nil?
145
151
 
146
- self.attributes = JSON.parse(object.data, :max_nesting => false)
152
+ self.attributes = Yajl::Parser.parse(object.data)
147
153
 
148
154
  # load all other non-hidden files in the dir as blobs
149
155
  blobs = (GitModel.current_tree / dir).blobs.reject{|b| b.name[0] == '.' || b.name == 'attributes.json'}
@@ -184,25 +190,92 @@ module GitModel
184
190
  end
185
191
 
186
192
  def exists?(id)
193
+ GitModel.logger.debug "Checking existence of #{name} with id: #{id}"
187
194
  GitModel.repo.commits.any? && !(GitModel.current_tree / File.join(db_subdir, id, 'attributes.json')).nil?
188
195
  end
189
196
 
190
197
  def find_all(conditions = {})
198
+ # TODO Refactor this spaghetti
191
199
  GitModel.logger.debug "Finding all #{name.pluralize} with conditions: #{conditions.inspect}"
192
- results = []
193
- return results unless GitModel.current_tree
194
- dirs = (GitModel.current_tree / db_subdir).trees
195
- dirs.each do |dir|
196
- if dir.blobs.any?
197
- o = new
198
- o.send :load, File.join(db_subdir, dir.name)
199
- results << o
200
+ return [] unless GitModel.current_tree
201
+
202
+ order = conditions.delete(:order) || :asc
203
+ order_by = conditions.delete(:order_by) || :id
204
+ limit = conditions.delete(:limit)
205
+
206
+ matching_ids = []
207
+ if conditions.empty? # load all objects
208
+ trees = (GitModel.current_tree / db_subdir).trees
209
+ trees.each do |t|
210
+ matching_ids << t.name if t.blobs.any?
200
211
  end
212
+ else # only load objects that match conditions
213
+ matching_ids_for_condition = {}
214
+ conditions.each do |k,v|
215
+ matching_ids_for_condition[k] = []
216
+ if k == :id # id isn't indexed
217
+ if v.is_a?(Proc)
218
+ trees = (GitModel.current_tree / db_subdir).trees
219
+ trees.each do |t|
220
+ matching_ids_for_condition[k] << t.name if t.blobs.any? && v.call(t.name)
221
+ end
222
+ else
223
+ # an unlikely use case but supporting it for completeness
224
+ matching_ids_for_condition[k] << v if (GitModel.current_tree / db_subdir / v)
225
+ end
226
+ else
227
+ raise GitModel::IndexRequired unless index.generated?
228
+ attr_index = index.attr_index(k)
229
+ if v.is_a?(Proc)
230
+ attr_index.each do |value, ids|
231
+ matching_ids_for_condition[k] += ids.to_a if v.call(value)
232
+ end
233
+ else
234
+ matching_ids_for_condition[k] += attr_index[v].to_a
235
+ end
236
+ end
237
+ end
238
+ matching_ids += matching_ids_for_condition.values.inject{|memo, obj| memo & obj}
239
+ end
240
+
241
+ results = nil
242
+ if order_by != :id
243
+ GitModel.logger.warn "Ordering by an attribute other than id requires loading all matching objects before applying limit, this will be slow" if limit
244
+ results = matching_ids.map{|k| find(k)}
245
+
246
+ if order == :asc
247
+ results = results.sort{|a,b| a.send(order_by) <=> b.send(order_by)}
248
+ elsif order == :desc
249
+ results = results.sort{|b,a| a.send(order_by) <=> b.send(order_by)}
250
+ else
251
+ raise GitModel::InvalidParams("invalid order: '#{order}'")
252
+ end
253
+
254
+ if limit
255
+ results = results[0, limit]
256
+ end
257
+ else
258
+ if limit
259
+ matching_ids = matching_ids[0, limit]
260
+ end
261
+ if order == :asc
262
+ matching_ids = matching_ids.sort{|a,b| a <=> b}
263
+ elsif order == :desc
264
+ matching_ids = matching_ids.sort{|b,a| a <=> b}
265
+ else
266
+ raise GitModel::InvalidParams("invalid order: '#{order}'")
267
+ end
268
+ results = matching_ids.map{|k| find(k)}
201
269
  end
202
270
 
203
271
  return results
204
272
  end
205
273
 
274
+ def all_values_for_attr(attr)
275
+ attr_index = index.attr_index(attr.to_s)
276
+ values = attr_index ? attr_index.keys : []
277
+ end
278
+
206
279
  def create(args)
207
280
  if args.is_a?(Array)
208
281
  args.map{|arg| create(arg)}
@@ -224,6 +297,7 @@ module GitModel
224
297
  end
225
298
 
226
299
  def delete(id, options = {})
300
+ GitModel.logger.debug "Deleting #{name} with id: #{id}"
227
301
  path = File.join(db_subdir, id)
228
302
  transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
229
303
  result = transaction.execute do |t|
@@ -232,12 +306,18 @@ module GitModel
232
306
  end
233
307
 
234
308
  def delete_all(options = {})
309
+ GitModel.logger.debug "Deleting all #{name.pluralize}"
235
310
  transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
236
311
  result = transaction.execute do |t|
237
312
  delete_tree(db_subdir, t.index, options)
238
313
  end
239
314
  end
240
315
 
316
+ def index!
317
+ index.generate!
318
+ index.save
319
+ end
320
+
241
321
 
242
322
  private
243
323
 
data/lib/gitmodel.rb CHANGED
@@ -4,12 +4,13 @@ require 'bundler/setup'
4
4
  require 'active_model'
5
5
  require 'active_support/all' # TODO we don't really want all here, clean this up
6
6
  require 'grit'
7
- require 'json'
7
+ require 'yajl'
8
8
  require 'lockfile'
9
9
  require 'pp'
10
10
 
11
11
  $:.unshift(File.dirname(__FILE__))
12
12
  require 'gitmodel/errors'
13
+ require 'gitmodel/index'
13
14
  require 'gitmodel/persistable'
14
15
  require 'gitmodel/transaction'
15
16
 
@@ -73,4 +74,10 @@ module GitModel
73
74
  c ? c.tree : nil
74
75
  end
75
76
 
77
+ def self.index!
78
+ dirs = (GitModel.current_tree).trees
79
+ dirs.each do |dir|
80
+ dir.name.classify.constantize.index!
81
+ end
82
+ end
76
83
  end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ describe GitModel::Index do
4
+ before(:each) do
5
+ TestEntity.create!(:id => "foo", :attributes => {:x => 1, :y => 2})
6
+ TestEntity.create!(:id => "bar", :attributes => {:x => 1, :y => 3})
7
+ TestEntity.create!(:id => "baz", :attributes => {:x => 2, :y => 2})
8
+
9
+ @i = GitModel::Index.new(TestEntity)
10
+ @i.generate!
11
+ end
12
+
13
+ it "has a hash for each attribute of the model" do
14
+ @i.attr_index(:x).should be_a(Hash)
15
+ end
16
+
17
+ it "knows the id's of all instances with a given value for an attribute" do
18
+ @i.attr_index(:x)[1].should == SortedSet.new(["foo", "bar"])
19
+ @i.attr_index(:x)[2].should == SortedSet.new(["baz"])
20
+ @i.attr_index(:y)[2].should == SortedSet.new(["foo", "baz"])
21
+ end
22
+
23
+ it "can regenerate itself" do
24
+ @i.attr_index(:x).clear
25
+ @i.attr_index(:x).should be_empty
26
+ @i.generate!
27
+ @i.attr_index(:x).should == {1 => SortedSet.new(["foo", "bar"]), 2 => SortedSet.new(["baz"])}
28
+ end
29
+
30
+ it "knows it's filename" do
31
+ @i.filename.should == "test_entities/_indexes.json"
32
+ end
33
+
34
+ it "can save itself to a JSON file" do
35
+ @i.save
36
+ json = <<-END.strip
37
+ [
38
+ [
39
+ "x",
40
+ [
41
+ [
42
+ 1,
43
+ [
44
+ "bar",
45
+ "foo"
46
+ ]
47
+ ],
48
+ [
49
+ 2,
50
+ [
51
+ "baz"
52
+ ]
53
+ ]
54
+ ]
55
+ ],
56
+ [
57
+ "y",
58
+ [
59
+ [
60
+ 3,
61
+ [
62
+ "bar"
63
+ ]
64
+ ],
65
+ [
66
+ 2,
67
+ [
68
+ "baz",
69
+ "foo"
70
+ ]
71
+ ]
72
+ ]
73
+ ]
74
+ ]
75
+ END
76
+ repo = Grit::Repo.new(GitModel.db_root)
77
+ # We should be able to use just repo.commits.first here but
78
+ # this is a workaround for this bug:
79
+ # http://github.com/mojombo/grit/issues/issue/38
80
+ (repo.commits("master^..master").first.tree / @i.filename).data.should == json
81
+ end
82
+
83
+ it "can save and load itself from a file" do
84
+ @i.save
85
+ @i.attr_index(:x).clear
86
+ @i.load
87
+ @i.attr_index(:x).should == {1 => SortedSet.new(["foo", "bar"]), 2 => SortedSet.new(["baz"])}
88
+ end
89
+
90
+ describe "#attr_index" do
91
+ it "loads itself" do
92
+ i = GitModel::Index.new(TestEntity)
93
+ i.should_receive(:load)
94
+ i.attr_index(:foo)
95
+ end
96
+
97
+ describe "with an index file already created" do
98
+ before(:each) { @i.save }
99
+
100
+ it "loads itself from file" do
101
+ i = GitModel::Index.new(TestEntity)
102
+ i.should_not_receive(:generate!)
103
+ i.attr_index(:x)
104
+ end
105
+ end
106
+ end
107
+
108
+ end
@@ -1,9 +1,5 @@
1
1
  require 'spec_helper'
2
2
 
3
- class TestEntity
4
- include GitModel::Persistable
5
- end
6
-
7
3
  class LintTest < ActiveModel::TestCase
8
4
  include ActiveModel::Lint::Tests
9
5
 
@@ -51,7 +47,7 @@ describe GitModel::Persistable do
51
47
 
52
48
  repo = Grit::Repo.new(GitModel.db_root)
53
49
  attrs = (repo.commits.first.tree / File.join(TestEntity.db_subdir, id, 'attributes.json')).data
54
- r = JSON.parse(attrs)
50
+ r = Yajl::Parser.parse(attrs)
55
51
  r.size.should == 2
56
52
  r['one'].should == 1
57
53
  r['two'].should == 2
@@ -83,6 +79,10 @@ describe GitModel::Persistable do
83
79
  it 'returns the SHA of the commit if the save was successful'
84
80
 
85
81
  it 'deletes blobs that have been removed'
82
+
83
+ it 'updates the index' do
84
+ # TODO
85
+ end
86
86
  end
87
87
 
88
88
  describe '#save!' do
@@ -223,6 +223,7 @@ describe GitModel::Persistable do
223
223
  TestEntity.create!(:id => 'ape')
224
224
 
225
225
  TestEntity.delete_all
226
+ TestEntity.index!
226
227
  TestEntity.find_all.should be_empty
227
228
  end
228
229
 
@@ -246,7 +247,7 @@ describe GitModel::Persistable do
246
247
 
247
248
  end
248
249
 
249
- describe '#find' do
250
+ describe '.find' do
250
251
 
251
252
  #it 'can load an object from an empty subdir of db_root' do
252
253
  # id = "foo"
@@ -316,25 +317,158 @@ describe GitModel::Persistable do
316
317
 
317
318
  end
318
319
 
319
- describe '#find_all' do
320
+ describe '.find_all' do
321
+ describe 'with no parameters' do
322
+ it 'returns an array of all objects' do
323
+ TestEntity.create!(:id => 'one')
324
+ TestEntity.create!(:id => 'two')
325
+ TestEntity.create!(:id => 'three')
320
326
 
321
- it 'returns an array of all objects' do
322
- TestEntity.create!(:id => 'one')
323
- TestEntity.create!(:id => 'two')
324
- TestEntity.create!(:id => 'three')
327
+ r = TestEntity.find_all
328
+ r.size.should == 3
329
+ end
330
+
331
+ it 'returns an empty array if there are no objects of the current type' do
332
+ r = TestEntity.find_all
333
+ r.should == []
334
+ end
335
+ end
336
+
337
+ describe 'with conditions but no index' do
338
+ it 'raises an exception' do
339
+ TestEntity.create!(:id => 'one')
340
+ lambda {TestEntity.find_all(:a => "b")}.should raise_error(GitModel::IndexRequired)
341
+ end
342
+ end
343
+
344
+ describe 'with one condition' do
345
+ describe 'with a literal value' do
346
+ it 'returns an array of all objects that match' do
347
+ TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
348
+ TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
349
+ TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
350
+ TestEntity.index!
351
+
352
+ r = TestEntity.find_all(:a => 1)
353
+ r.size.should == 2
354
+ r.first.id.should == 'one'
355
+ r.second.id.should == 'three'
356
+ end
357
+ end
358
+
359
+ describe 'with a lambda as the value' do
360
+ it 'returns an array of all objects that match' do
361
+ TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
362
+ TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
363
+ TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
364
+ TestEntity.index!
365
+
366
+ r = TestEntity.find_all(:b => lambda{|b| b > 1}, :order => :asc)
367
+ r.size.should == 2
368
+ r.first.id.should == 'three'
369
+ r.second.id.should == 'two'
370
+ end
371
+ end
372
+ end
373
+
374
+ describe 'with multiple conditions' do
375
+ describe 'with a literal value' do
376
+ it 'returns an array of all objects that match both (i.e. AND)' do
377
+ TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 2})
378
+ TestEntity.create!(:id => 'two', :attributes => {:a => 1, :b => 2})
379
+ TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 1})
380
+ TestEntity.index!
381
+
382
+ r = TestEntity.find_all(:a => 1, :b => 2, :order => :asc)
383
+ r.size.should == 2
384
+ r.first.id.should == 'one'
385
+ r.second.id.should == 'two'
386
+ end
387
+ end
388
+
389
+ describe 'with a lambda as the value' do
390
+ it 'returns an array of all objects that match both (i.e. AND)' do
391
+ TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 3})
392
+ TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
393
+ TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 1})
394
+ TestEntity.create!(:id => 'four', :attributes => {:a => 3, :b => 3})
395
+ TestEntity.index!
396
+
397
+ r = TestEntity.find_all(:a => lambda{|a| a > 1}, :b => lambda{|b| b > 2}, :order => :asc)
398
+ r.size.should == 1
399
+ r.first.id.should == 'four'
400
+ end
401
+
402
+ end
403
+ end
404
+
405
+ describe 'with the id attribute in the conditions' do
406
+ describe 'with a literal value' do
407
+ it 'returns an array that includes the object with that id' do
408
+ TestEntity.create!(:id => 'one')
409
+ TestEntity.create!(:id => 'two')
410
+ TestEntity.create!(:id => 'three')
411
+
412
+ r = TestEntity.find_all(:id => 'one')
413
+ r.size.should == 1
414
+ r.first.id.should == 'one'
415
+ end
416
+ end
417
+
418
+ describe 'with a lambda as the value' do
419
+ it 'returns an array that includes the object with that id' do
420
+ TestEntity.create!(:id => 'one')
421
+ TestEntity.create!(:id => 'two')
422
+ TestEntity.create!(:id => 'three')
423
+
424
+ r = TestEntity.find_all(:id => lambda{|id| id =~ /o/})
425
+ r.size.should == 2
426
+ r.first.id.should == 'one'
427
+ r.second.id.should == 'two'
428
+ end
429
+
430
+ end
431
+ end
432
+
433
+ it 'can return results in ascending order' do
434
+ TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
435
+ TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
436
+ TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
437
+ TestEntity.index!
438
+
439
+ r = TestEntity.find_all(:a => 1, :order => :asc)
440
+ r.size.should == 2
441
+ r.first.id.should == 'one'
442
+ r.second.id.should == 'three'
443
+ end
444
+
445
+ it 'can return results in descending order' do
446
+ TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
447
+ TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
448
+ TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
449
+ TestEntity.index!
325
450
 
326
- r = TestEntity.find_all
327
- r.size.should == 3
451
+ r = TestEntity.find_all(:a => 1, :order => :desc)
452
+ r.size.should == 2
453
+ r.first.id.should == 'three'
454
+ r.second.id.should == 'one'
328
455
  end
329
456
 
330
- it 'returns an empty array if there are no objects of the current type' do
331
- r = TestEntity.find_all
332
- r.should == []
457
+ it 'can limit the number of results returned' do
458
+ TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
459
+ TestEntity.create!(:id => 'two', :attributes => {:a => 1, :b => 2})
460
+ TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
461
+ TestEntity.index!
462
+
463
+ r = TestEntity.find_all(:a => 1, :limit => 2)
464
+ r.size.should == 2
465
+ r.first.id.should == 'one'
466
+ r.second.id.should == 'three'
333
467
  end
334
468
 
335
469
  end
336
470
 
337
- describe '#exists?' do
471
+ describe '.exists?' do
338
472
 
339
473
  it 'returns true if the record exists' do
340
474
  TestEntity.create!(:id => 'one')
@@ -347,6 +481,23 @@ describe GitModel::Persistable do
347
481
 
348
482
  end
349
483
 
484
+ describe ".index!" do
485
+ it "generates and saves the index" do
486
+ TestEntity.index.should_receive(:generate!)
487
+ TestEntity.index.should_receive(:save)
488
+ TestEntity.index!
489
+ end
490
+ end
491
+
492
+ describe '.all_values_for_attr' do
493
+ it 'returns a list of all values that exist for a given attribute' do
494
+ o = TestEntity.create!(:id => 'first', :attributes => {"a" => 1, "b" => 2})
495
+ o = TestEntity.create!(:id => 'second', :attributes => {"a" => 3, "b" => 4})
496
+ TestEntity.index!
497
+ TestEntity.all_values_for_attr("a").should == [1, 3]
498
+ end
499
+ end
500
+
350
501
  describe '#attributes' do
351
502
  it 'accepts symbols or strings interchangeably as strings' do
352
503
  o = TestEntity.new(:id => 'lol', :attributes => {"one" => 1, :two => 2})
@@ -2,8 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
3
  describe GitModel do
4
4
 
5
- describe "#last_commit" do
6
-
5
+ describe ".last_commit" do
7
6
  it "returns nil if there are no commits" do
8
7
  GitModel.last_commit.should == nil
9
8
  end
@@ -17,11 +16,9 @@ describe GitModel do
17
16
 
18
17
  GitModel.last_commit.to_s.should == sha
19
18
  end
20
-
21
19
  end
22
20
 
23
- describe "#current_tree" do
24
-
21
+ describe ".current_tree" do
25
22
  it "returns nil if there are no commits" do
26
23
  GitModel.current_tree.should == nil
27
24
  end
@@ -32,7 +29,19 @@ describe GitModel do
32
29
  GitModel.should_receive(:last_commit).with('master').and_return(last_commit)
33
30
  GitModel.current_tree('master')
34
31
  end
32
+ end
35
33
 
34
+ describe ".index" do
35
+ before(:each) do
36
+ TestEntity.create!(:id => "foo")
37
+ TestEntity2.create!(:id => "bar")
38
+ end
39
+
40
+ it "calls index! on each Persistable model class" do
41
+ TestEntity.should_receive(:index!)
42
+ TestEntity2.should_receive(:index!)
43
+ GitModel.index!
44
+ end
36
45
  end
37
46
 
38
47
  end
data/spec/spec_helper.rb CHANGED
@@ -6,7 +6,15 @@ require 'gitmodel'
6
6
 
7
7
  Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
8
8
 
9
- Rspec.configure do |c|
9
+ RSpec.configure do |c|
10
10
  c.mock_with :rspec
11
11
  end
12
12
 
13
+ class TestEntity
14
+ include GitModel::Persistable
15
+ end
16
+ class TestEntity2
17
+ include GitModel::Persistable
18
+ end
19
+
20
+ #GitModel.logger.level = ::Logger::DEBUG
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: gitmodel
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.2
5
+ version: 0.0.3
6
6
  platform: ruby
7
7
  authors:
8
8
  - Paul Dowman
@@ -10,8 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-02-19 00:00:00 -05:00
14
- default_executable:
13
+ date: 2011-07-04 00:00:00 Z
15
14
  dependencies:
16
15
  - !ruby/object:Gem::Dependency
17
16
  name: activemodel
@@ -58,36 +57,36 @@ dependencies:
58
57
  type: :runtime
59
58
  version_requirements: *id004
60
59
  - !ruby/object:Gem::Dependency
61
- name: ZenTest
60
+ name: yajl-ruby
62
61
  prerelease: false
63
62
  requirement: &id005 !ruby/object:Gem::Requirement
64
63
  none: false
65
64
  requirements:
66
65
  - - ">="
67
66
  - !ruby/object:Gem::Version
68
- version: 4.4.0
69
- type: :development
67
+ version: 0.8.2
68
+ type: :runtime
70
69
  version_requirements: *id005
71
70
  - !ruby/object:Gem::Dependency
72
- name: autotest
71
+ name: ZenTest
73
72
  prerelease: false
74
73
  requirement: &id006 !ruby/object:Gem::Requirement
75
74
  none: false
76
75
  requirements:
77
76
  - - ">="
78
77
  - !ruby/object:Gem::Version
79
- version: 4.4.1
78
+ version: 4.4.0
80
79
  type: :development
81
80
  version_requirements: *id006
82
81
  - !ruby/object:Gem::Dependency
83
- name: autotest-fsevent
82
+ name: autotest
84
83
  prerelease: false
85
84
  requirement: &id007 !ruby/object:Gem::Requirement
86
85
  none: false
87
86
  requirements:
88
87
  - - ">="
89
88
  - !ruby/object:Gem::Version
90
- version: 0.2.3
89
+ version: 4.4.1
91
90
  type: :development
92
91
  version_requirements: *id007
93
92
  - !ruby/object:Gem::Dependency
@@ -122,14 +121,15 @@ files:
122
121
  - gitmodel.gemspec
123
122
  - lib/gitmodel.rb
124
123
  - lib/gitmodel/errors.rb
124
+ - lib/gitmodel/index.rb
125
125
  - lib/gitmodel/persistable.rb
126
126
  - lib/gitmodel/transaction.rb
127
+ - spec/gitmodel/index_spec.rb
127
128
  - spec/gitmodel/persistable_spec.rb
128
129
  - spec/gitmodel/transaction_spec.rb
129
130
  - spec/gitmodel_spec.rb
130
131
  - spec/spec_helper.rb
131
132
  - spec/support/setup.rb
132
- has_rdoc: true
133
133
  homepage: http://github.com/pauldowman/gitmodel
134
134
  licenses: []
135
135
 
@@ -153,11 +153,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
153
  requirements: []
154
154
 
155
155
  rubyforge_project:
156
- rubygems_version: 1.5.0
156
+ rubygems_version: 1.8.5
157
157
  signing_key:
158
158
  specification_version: 3
159
159
  summary: An ActiveModel-compliant persistence framework for Ruby that uses Git for versioning and remote syncing.
160
160
  test_files:
161
+ - spec/gitmodel/index_spec.rb
161
162
  - spec/gitmodel/persistable_spec.rb
162
163
  - spec/gitmodel/transaction_spec.rb
163
164
  - spec/gitmodel_spec.rb