campchair 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ tmp/ccdb
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --order random
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.3
5
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in campchair.gemspec
4
+ gemspec
@@ -0,0 +1,273 @@
1
+ # ⑁ Campchair
2
+ ### Lightweight, ghetto-tech CouchDB-like views in Ruby
3
+
4
+ [![Build Status](https://secure.travis-ci.org/captainpete/campchair.png?branch=master)](http://travis-ci.org/captainpete/campchair)
5
+
6
+ ___This README is a wishlist. The thing doesn't entirely work yet!___
7
+
8
+ Campchair provides some helpers for lazily materialized views and a
9
+ persistence mapping for different Ruby types.
10
+
11
+ Views are described a bit like those found in [CouchDB](http://couchdb.apache.org/).
12
+ Campchair is not a service and has no client-server stuff.
13
+ Parallel writes are not supported, but that's okay – you can use something
14
+ else for concurrency and use this in your persistence fiber/thread/process.
15
+ Persistence happens through [LevelDB](http://code.google.com/p/leveldb/).
16
+ This is not append-only and replication is not a feature.
17
+ For these reasons it's called campchair, not couch :)
18
+
19
+ This project was born at [Railscamp 11](http://railscamps.com/) for the heavy-metrics project.
20
+
21
+ ### How does it look?
22
+
23
+ For these examples we'll work with two documents.
24
+
25
+ ```ruby
26
+ fred = {
27
+ :name => 'Fred', :height => 1.748,
28
+ :location => 'Flinders St Station, Melbourne, Australia',
29
+ :first_seen => Time.new(2012, 3, 6, 12, 10),
30
+ :last_seen => Time.new(2012, 3, 6, 12, 40)
31
+ }
32
+ jane = {
33
+ :name => 'Jane', :height => 1.634,
34
+ :location => 'Flinders St Station, Melbourne, Australia',
35
+ :first_seen => Time.new(2012, 3, 6, 12, 20),
36
+ :last_seen => Time.new(2012, 3, 6, 12, 50)
37
+ }
38
+ ```
39
+
40
+ Mixing in Campchair DB behavior to a class is as simple as...
41
+
42
+ ```ruby
43
+ class People
44
+ include Campchair::LevelDB
45
+ end
46
+ ```
47
+
48
+ Now, use it like a `Hash`.
49
+
50
+ ```ruby
51
+ # Add a document to the db
52
+ id = People << fred
53
+
54
+ # Add another person
55
+ People << jane
56
+
57
+ # Rerieve a document from the db
58
+ fred = People[id]
59
+
60
+ # Delete the document from the db
61
+ People.delete(id)
62
+
63
+ # Update/create the document in the db using an id
64
+ fred[:last_seen] = Time.new(2012, 3, 6, 12, 40, 25)
65
+ doc[id] = fred
66
+ ```
67
+
68
+ ### Basic views _not yet implemented_
69
+
70
+ For views to be available on classes we need to include `Campchair::Views`.
71
+
72
+ ```ruby
73
+ class People # add some views to the people class
74
+ include Campchair::Views
75
+
76
+ # Map to document by id
77
+ view :all do
78
+ map { |id, person| emit id, person }
79
+ end
80
+
81
+ # Map to name by doc id
82
+ view :names do
83
+ map { |id, person| emit id, person.name }
84
+ end
85
+ end
86
+
87
+ People.names
88
+ # ['Fred', 'Jane']
89
+ ```
90
+
91
+ ### Using reduce _not yet implemented_
92
+
93
+ Reduce gets called on groups of map results.
94
+
95
+ ```ruby
96
+ class People
97
+ # Map to count by doc id
98
+ # Reduce to sum of counts
99
+ view :count do
100
+ map { |id, person| emit id, 1 }
101
+ reduce do |keys, values|
102
+ values.inject(0) { |memo, values| memo + values }
103
+ end
104
+ end
105
+ end
106
+
107
+ People.count
108
+ # 2
109
+ ```
110
+
111
+ Reduce happens in stages. Supply a rereduce step to handle reducing reduce
112
+ results. The advantage of rereduce is results can be cached in a b-tree index.
113
+
114
+ If `rereduce` is supplied then `reduce` is called with a portion of the `map`
115
+ results and those reductions are passed to `rereduce`. If `rereduce` is
116
+ omitted then `reduce` results will not be cached.
117
+
118
+ ```ruby
119
+ class People
120
+ # Map to location by doc id
121
+ # Reduce by unique location
122
+ view :unique_locations do
123
+ map { |id, person| emit id, person.location }
124
+ reduce do |keys, values|
125
+ results = values.uniq
126
+ end
127
+ rereduce do |results|
128
+ results.inject(Set.new) { |memo, values| memo.merge values }
129
+ end
130
+ end
131
+ end
132
+
133
+ People.unique_locations
134
+ # <Set: {"Flinders St Station, Melbourne, Australia"}>
135
+ ```
136
+
137
+ Keep in mind that if `rereduce` is not supplied, `reduce` will always get
138
+ called with the entire keyspace. If the dataset is large this might consume a
139
+ lot of memory.
140
+
141
+ ### Using keys with reduce _not yet implemented_
142
+
143
+ ```ruby
144
+ class People
145
+ # Map to count by name
146
+ # Reduce by sum of counts
147
+ view :count_by_name do
148
+ map { |id, person| emit name, 1 }
149
+ reduce do |keys, values|
150
+ values.inject(0) { |memo, value| memo + value }
151
+ end
152
+ rereduce do |results|
153
+ results.inject(0) { |memo, value| memo + value }
154
+ end
155
+ end
156
+ end
157
+
158
+ People.count_by_name['Jane']
159
+ # 1
160
+ People.count_by_name
161
+ # 2
162
+ ```
163
+
164
+ ### Controlling the index _not yet relevant_
165
+
166
+ It's possible to write reduce methods that return results larger than the
167
+ input. If you're doing this, you're gonna have a bad time. Unless you're sure
168
+ the dataset will remain small enough that the index for each `rereduce` doesn't
169
+ blow out your disk store.
170
+
171
+ ```ruby
172
+ class People
173
+ # DANGER: this view will blow out the index
174
+
175
+ # Map to count by name
176
+ # Reduce by sum of counts by name
177
+ view :count_all_by_name do
178
+ map { |id, person| emit person.name, 1 }
179
+ reduce do |keys, values|
180
+ result = Hash.new
181
+ keys.each_with_index do |key, index|
182
+ result[key] ||= 0
183
+ result[key] += values[index]
184
+ end
185
+ result # scary, nearly as large as the input
186
+ end
187
+ rereduce(:cache => false) do |results|
188
+ result = {}
189
+ results.each do |name, count|
190
+ result[name] ||= 0
191
+ result[name] += count
192
+ end
193
+ result # likewise, this will explode the index
194
+ end
195
+ end
196
+ end
197
+
198
+ People.count_all_by_name['Jane']
199
+ # { 'Jane' => 1 }
200
+ People.count_all_by_name
201
+ # { 'Fred' => 1, 'Jane' => 1 }
202
+ ```
203
+
204
+ ### Re-re-reducing _not yet implemented_
205
+
206
+ Sometimes it's useful to post-process reduce results. Add another rereduce to
207
+ process rereduced values. Only the first rereduce gets called on values it
208
+ produces itself. Subsequent rereduces are chained to the initial result.
209
+
210
+ ```ruby
211
+ clas People
212
+ # Map to height by location
213
+ # Reduce by count and sum of heights
214
+ # Rereduce by the calculating average
215
+ view :average_height_by_location do
216
+ map { |id, person| emit location, height }
217
+ reduce do |keys, values|
218
+ result = Hash.new
219
+ keys.each_with_index do |key, index|
220
+ result[key] ||= { :count => 0, :sum => 0 }
221
+ result[key][:count] += 1
222
+ result[key][:sum] += height
223
+ end
224
+ result
225
+ end
226
+ rereduce do |results|
227
+ result = { :count => 0, :sum => 0 }
228
+ results.each do |result|
229
+ result[:count] += part[:count]
230
+ result[:sum] += part[:sum]
231
+ end
232
+ result
233
+ end
234
+ rereduce do |results|
235
+ count = results.inject(0) { |memo, result| result[:count] }
236
+ sum = results.inject(0) { |memo, result| result[:sum] }
237
+ count == 0 ? nil : sum / count
238
+ end
239
+ end
240
+ end
241
+
242
+ People.average_height_by_location
243
+ # 1.690999999
244
+ People.average_height_by_location['Flinders St Station, Melbourne, Australia']
245
+ # 1.690999999
246
+ People.average_height_by_location['Docklands, Melbourne, Australia']
247
+ # nil
248
+ ```
249
+
250
+ ### Custom database path
251
+
252
+ By default, the folder for db files is 'cddb'.
253
+ You can change this with:
254
+ ```ruby
255
+ Campchair.db_path = 'db/heavy_metrics'
256
+ ```
257
+
258
+ You can also change the db on a per-class basis.
259
+ ```ruby
260
+ class Person
261
+ include Campchair::LevelDB
262
+ self.db_path = 'db/heavy_metrics/Person'
263
+ end
264
+ ```
265
+
266
+ ### TODO
267
+
268
+ - Make the README examples work
269
+ - Views spanning views
270
+ - Caching view results
271
+ - Caching reduce results
272
+ - method_missing for `count_by_*, sum_of_*s, min_*, max_*, unique_*s`
273
+ - cache priming
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core'
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec) do |spec|
6
+ spec.pattern = FileList['spec/**/*_spec.rb']
7
+ end
8
+
9
+ task :default => :spec
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "campchair/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "campchair"
7
+ s.version = Campchair::VERSION
8
+ s.authors = ["Peter Hollows"]
9
+ s.email = ["pete@dojo7.com"]
10
+ s.homepage = "https://github.com/captainpete/campchair"
11
+ s.summary = %q{Lightweight, ghetto-tech CouchDB-like views in Ruby}
12
+ s.description = %q{Campchair is a map-reduce framework using levelDB for persistence, and Ruby for everything else.}
13
+
14
+ s.rubyforge_project = "campchair"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency(%q<leveldb-ruby>, ["~> 0.14"])
22
+ s.add_development_dependency(%q<rspec>)
23
+ s.add_development_dependency(%q<rake>)
24
+ end
@@ -0,0 +1,14 @@
1
+ require "campchair/version"
2
+
3
+ module Campchair
4
+ autoload :Views, 'campchair/views'
5
+ autoload :LevelDB, 'campchair/leveldb'
6
+
7
+ class << self
8
+ attr_writer :db_path
9
+
10
+ def db_path
11
+ @db_path || 'ccdb'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,70 @@
1
+ require 'leveldb'
2
+ require 'digest/sha1'
3
+
4
+ module Campchair
5
+ module LevelDB
6
+ class << self
7
+ def generate_key
8
+ @alpha ||= [('a'..'f'), ('0'..'9')].map(&:to_a).flatten
9
+ (0...40).map { @alpha[rand(@alpha.size)] }.join
10
+ end
11
+
12
+ def encode(value)
13
+ Marshal.dump(value)
14
+ end
15
+
16
+ def decode(value)
17
+ Marshal.load(value)
18
+ end
19
+
20
+ def included(base)
21
+ base.class_eval do
22
+ class << self
23
+ attr_writer :db_path, :db_name, :db
24
+
25
+ def db_path
26
+ @db_path || File.join(Campchair.db_path, db_name)
27
+ end
28
+
29
+ def db_name
30
+ @db_name || name
31
+ end
32
+
33
+ def db
34
+ @db ||= create_db
35
+ end
36
+
37
+ def [](key)
38
+ val = db[key]
39
+ val && Campchair::LevelDB.decode(db[key])
40
+ end
41
+
42
+ def []=(key, value)
43
+ db[key] = Campchair::LevelDB.encode(value)
44
+ end
45
+
46
+ def <<(value)
47
+ id = Campchair::LevelDB.generate_key
48
+ self[id] = value
49
+ return id
50
+ end
51
+
52
+ def delete(key)
53
+ db.delete(key)
54
+ end
55
+
56
+ protected
57
+
58
+ def create_db
59
+ FileUtils.mkdir_p(db_path)
60
+ ::LevelDB::DB.new(db_path)
61
+ end
62
+
63
+ end # class << self
64
+
65
+ end # base.class_eval
66
+ end # included
67
+ end # class << self
68
+
69
+ end # module LevelDB
70
+ end # module Campchair
@@ -0,0 +1,3 @@
1
+ module Campchair
2
+ VERSION = "0.0.4"
3
+ end
@@ -0,0 +1,4 @@
1
+ module Campchair
2
+ module Views
3
+ end
4
+ end
File without changes
@@ -0,0 +1,139 @@
1
+ require 'spec_helper'
2
+
3
+ describe Campchair::LevelDB do
4
+ let(:test_module) do
5
+ Campchair::LevelDB.dup
6
+ end
7
+
8
+ describe ".generate_key" do
9
+ it "generates a hex key 40 characters long" do
10
+ test_module.generate_key.should =~ /\A[a-f0-9]{40}\Z/
11
+ end
12
+ end
13
+
14
+ context "included" do
15
+ let(:test_class) do
16
+ Object.send(:remove_const, :TestEntity) if defined? TestEntity
17
+ TestEntity = Class.new do
18
+ include Campchair::LevelDB
19
+ end
20
+ end
21
+
22
+ after do
23
+ `rm -fr tmp/ccdb`
24
+ end
25
+
26
+ describe "setting the db_name" do
27
+ describe ".db_name" do
28
+ it "is the class name by default" do
29
+ test_class.stub! :name => 'TestEntity'
30
+ test_class.db_name.should == 'TestEntity'
31
+ end
32
+ end
33
+
34
+ describe ".db_name=" do
35
+ it "is the custom db_name specified" do
36
+ test_class.db_name = 'TestEntityCustomName'
37
+ test_class.db_name.should == 'TestEntityCustomName'
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "setting the db_path" do
43
+ describe ".db_path" do
44
+ it "is the Campchair.db_path, and the db_name by default" do
45
+ Campchair.should_receive(:db_path).and_return('some/path')
46
+ test_class.db_path.should == 'some/path/TestEntity'
47
+ end
48
+ end
49
+
50
+ describe ".db_path=" do
51
+ it "is the custom db_path specified" do
52
+ test_class.db_path = 'some/other/path'
53
+ test_class.db_path.should == 'some/other/path'
54
+ end
55
+ end
56
+ end
57
+
58
+ context "with test db_path set" do
59
+ before do
60
+ test_class.db_path = 'tmp/ccdb/test'
61
+ end
62
+
63
+ describe ".db" do
64
+ it "is a LevelDB, at the db_path" do
65
+ test_class.db.should be_a(LevelDB::DB)
66
+ test_class.db.pathname.should == 'tmp/ccdb/test'
67
+ end
68
+ end
69
+
70
+ context "no data serialization" do
71
+ before do
72
+ # Passthrough {en,de}coding for these tests
73
+ Campchair::LevelDB.stub(:encode).and_return {|v| v}
74
+ Campchair::LevelDB.stub(:decode).and_return {|v| v}
75
+
76
+ FileUtils.mkdir_p('tmp/ccdb/TestEntity')
77
+ @db = ::LevelDB::DB.new('tmp/ccdb/TestEntity')
78
+
79
+ test_class.stub! :db => @db
80
+ end
81
+
82
+ describe ".[]" do
83
+ it "is nil when there's no matching key in the database" do
84
+ test_class['nonexistent'].should be_nil
85
+ end
86
+
87
+ it "is the value of the matching key in the database" do
88
+ @db['something'] = '22'
89
+ test_class['something'].should == '22'
90
+ end
91
+ end
92
+
93
+ describe ".[]=" do
94
+ it "sets the value of the key in the database" do
95
+ test_class['something'] = '22'
96
+ @db['something'].should == '22'
97
+ end
98
+ end
99
+
100
+ describe ".<<" do
101
+ it "adds the value to the database, generating a key" do
102
+ key = 'b78aa2f6b718651743fac2682003f2c63340ae34b'
103
+ Campchair::LevelDB.stub! :generate_key => key
104
+
105
+ id = TestEntity << '22'
106
+ id.should == key
107
+ test_class[id].should == '22'
108
+ end
109
+ end
110
+
111
+ describe ".delete" do
112
+ it "deletes the key its values from the db" do
113
+ test_class['something'] = '22'
114
+ test_class.delete('something')
115
+
116
+ test_class['something'].should be_nil
117
+ end
118
+ end
119
+ end
120
+
121
+ context "Campchair::LevelDB serialization" do
122
+ describe ".[]{,=}" do
123
+ it "persists non-string data structures" do
124
+ doc = { :sym => :sym, 'string' => 'string', 'types' => [1234.12341234, (55..44), /regexen/, Time.now, { 'a' => 22 }] }
125
+ test_class['something'] = doc
126
+ test_class['something'].should == doc
127
+ end
128
+ end
129
+
130
+ describe ".[]" do
131
+ it "is nil when the document does not exist" do
132
+ test_class['something'].should be_nil
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ end # context "included"
139
+ end # describe Campchair::LevelDB
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Campchair::Views do
4
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe Campchair do
4
+ let(:test_module) do
5
+ Campchair::dup
6
+ end
7
+
8
+ describe ".db_path" do
9
+ it "is 'ccdb' by default" do
10
+ test_module.db_path.should == 'ccdb'
11
+ end
12
+ end
13
+
14
+ describe ".db_path=" do
15
+ it "sets the default LevelDB for Campchair classes" do
16
+ test_module.db_path = 'tmp/ccdb'
17
+ test_module.db_path.should == 'tmp/ccdb'
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe "DB behavior" do
4
+ before do
5
+ @fred = {
6
+ :name => 'Fred', :height => 1.748,
7
+ :location => 'Flinders St Station, Melbourne, Australia',
8
+ :first_seen => Time.local(2012, 3, 6, 12, 10),
9
+ :last_seen => Time.local(2012, 3, 6, 12, 40)
10
+ }
11
+ @jane = {
12
+ :name => 'Jane', :height => 1.634,
13
+ :location => 'Flinders St Station, Melbourne, Australia',
14
+ :first_seen => Time.local(2012, 3, 6, 12, 20),
15
+ :last_seen => Time.local(2012, 3, 6, 12, 50)
16
+ }
17
+
18
+ class TestPeople
19
+ include Campchair::LevelDB
20
+ self.db_path = 'tmp/ccdb'
21
+ end
22
+ end
23
+
24
+ after do
25
+ `rm -fr tmp/ccdb`
26
+ end
27
+
28
+ it "persists documents without an id" do
29
+ id = TestPeople << @fred
30
+ TestPeople << @jane
31
+ end
32
+
33
+ it "retrieves documents" do
34
+ id = TestPeople << @fred
35
+ TestPeople[id].should == @fred
36
+ end
37
+
38
+ it "deletes documents" do
39
+ id = TestPeople << @fred
40
+ TestPeople.delete(id)
41
+ TestPeople[id].should be_nil
42
+ end
43
+
44
+ it "updates docuemnts with an id" do
45
+ id = TestPeople << @fred
46
+ new_last_seen = Time.local(2012, 3, 6, 12, 50)
47
+ @fred[:last_seen] = new_last_seen
48
+ TestPeople[id] = @fred
49
+
50
+ TestPeople[id][:last_seen].should == new_last_seen
51
+ end
52
+
53
+ it "creates documents with an id" do
54
+ id = 'something'
55
+ TestPeople[id] = @fred
56
+
57
+ TestPeople[id].should == @fred
58
+ end
59
+
60
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'campchair'
File without changes
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: campchair
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Peter Hollows
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: leveldb-ruby
16
+ requirement: &70242209955760 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.14'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70242209955760
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70242209955340 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70242209955340
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &70242209954880 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70242209954880
47
+ description: Campchair is a map-reduce framework using levelDB for persistence, and
48
+ Ruby for everything else.
49
+ email:
50
+ - pete@dojo7.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - .rspec
57
+ - .travis.yml
58
+ - Gemfile
59
+ - README.md
60
+ - Rakefile
61
+ - campchair.gemspec
62
+ - lib/campchair.rb
63
+ - lib/campchair/leveldb.rb
64
+ - lib/campchair/version.rb
65
+ - lib/campchair/views.rb
66
+ - spec/.gitkeep
67
+ - spec/campchair/leveldb_spec.rb
68
+ - spec/campchair/views_spec.rb
69
+ - spec/campchair_spec.rb
70
+ - spec/integration/readme_examples_spec.rb
71
+ - spec/spec_helper.rb
72
+ - tmp/.gitkeep
73
+ homepage: https://github.com/captainpete/campchair
74
+ licenses: []
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ! '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project: campchair
93
+ rubygems_version: 1.8.17
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Lightweight, ghetto-tech CouchDB-like views in Ruby
97
+ test_files:
98
+ - spec/campchair/leveldb_spec.rb
99
+ - spec/campchair/views_spec.rb
100
+ - spec/campchair_spec.rb
101
+ - spec/integration/readme_examples_spec.rb
102
+ - spec/spec_helper.rb