gitmodel 0.0.2 → 0.0.3

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