campchair 0.0.4

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