dispaaro-couch 0.1
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 +11 -0
- data/Rakefile +15 -0
- data/gem.yml +13 -0
- data/lib/couch.rb +31 -0
- data/lib/couch/database.rb +137 -0
- data/lib/couch/doc_view.rb +77 -0
- data/lib/couch/document.rb +140 -0
- data/lib/couch/error.rb +27 -0
- data/lib/couch/extras.rb +12 -0
- data/lib/couch/log.rb +8 -0
- data/lib/couch/server.rb +40 -0
- data/lib/couch/view.rb +72 -0
- data/script/console +19 -0
- data/script/rebuild +7 -0
- data/script/watchr +48 -0
- data/test/couch.json +5 -0
- data/test/couch/basic_test.rb +67 -0
- data/test/couch/bulk_test.rb +51 -0
- data/test/couch/doc_view_test.rb +55 -0
- data/test/couch/post.rb +3 -0
- data/test/couch/view_test.rb +60 -0
- data/test/couch_test_helper.rb +31 -0
- data/test/test.rb +14 -0
- data/test/test_helper.rb +30 -0
- data/vex/gem.rake +36 -0
- data/vex/gem.rb +95 -0
- metadata +104 -0
data/.gitignore
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
load "#{File.dirname __FILE__}/vex/gem.rake"
|
2
|
+
|
3
|
+
task :default => :test
|
4
|
+
|
5
|
+
task :test do
|
6
|
+
sh "ruby test/test.rb"
|
7
|
+
end
|
8
|
+
|
9
|
+
task :rcov do
|
10
|
+
sh "cd test; rcov -o ../coverage -x gems -x ^test.rb test.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
task :rdoc do
|
14
|
+
sh "rdoc -o doc/rdoc"
|
15
|
+
end
|
data/gem.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
name: "dispaaro-couch"
|
2
|
+
version: "0.1"
|
3
|
+
summary: "A CouchDB handler"
|
4
|
+
description: "Yet another CouchDB handler"
|
5
|
+
homepage: http://github.com/radiospiel/dispaaro-couch
|
6
|
+
author: radiospiel
|
7
|
+
email: eno@open-lab.org
|
8
|
+
dependencies:
|
9
|
+
- etest
|
10
|
+
# - nokogiri
|
11
|
+
# - sanitize
|
12
|
+
# - htmlentities
|
13
|
+
# - json
|
data/lib/couch.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dlog'
|
3
|
+
require 'vex'
|
4
|
+
|
5
|
+
module Couch
|
6
|
+
def self.database
|
7
|
+
Thread.current[:database]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.database=(database)
|
11
|
+
Thread.current[:database] = database
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.connect(host, port, db, recreate=false)
|
15
|
+
self.database = Database.new host, port, db, recreate
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.disconnect
|
19
|
+
Thread.current[:database] = nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
load "#{File.dirname(__FILE__)}/couch/extras.rb"
|
24
|
+
|
25
|
+
load "#{File.dirname(__FILE__)}/couch/log.rb"
|
26
|
+
load "#{File.dirname(__FILE__)}/couch/database.rb"
|
27
|
+
load "#{File.dirname(__FILE__)}/couch/document.rb"
|
28
|
+
load "#{File.dirname(__FILE__)}/couch/server.rb"
|
29
|
+
load "#{File.dirname(__FILE__)}/couch/error.rb"
|
30
|
+
load "#{File.dirname(__FILE__)}/couch/view.rb"
|
31
|
+
load "#{File.dirname(__FILE__)}/couch/doc_view.rb"
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'uuid'
|
2
|
+
|
3
|
+
class Couch::Database
|
4
|
+
include Couch::Log
|
5
|
+
|
6
|
+
def memoized_views
|
7
|
+
@memoized_views ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(host, port, db, recreate = false)
|
11
|
+
@server = Couch::Server.new host, port
|
12
|
+
@db = db
|
13
|
+
|
14
|
+
if recreate
|
15
|
+
Couch::Error.ignore { @server.delete("/#{db}") }
|
16
|
+
@server.put("/#{db}/", "")
|
17
|
+
else
|
18
|
+
Couch::Error.ignore { @server.put("/#{db}/", "") }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# -- forward HTTP methods to server object.
|
23
|
+
|
24
|
+
def get(url)
|
25
|
+
@server.get("/#{@db}/#{url}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def post(url, data)
|
29
|
+
@server.post("/#{@db}/#{url}", data)
|
30
|
+
end
|
31
|
+
|
32
|
+
def put(url, data)
|
33
|
+
@server.put("/#{@db}/#{url}", data)
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete(url)
|
37
|
+
@server.delete("/#{@db}/#{url}")
|
38
|
+
end
|
39
|
+
|
40
|
+
# -- document CRUD methods
|
41
|
+
|
42
|
+
module Base
|
43
|
+
def create(doc)
|
44
|
+
raise unless doc.new_record?
|
45
|
+
|
46
|
+
doc.id ||= UUID.new.generate
|
47
|
+
|
48
|
+
r = put(doc.id, doc)
|
49
|
+
doc.send :update_rev, r[:rev]
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# read couch document from server
|
54
|
+
def read(id)
|
55
|
+
r = get(id)
|
56
|
+
Couch::Document.instantiate r
|
57
|
+
end
|
58
|
+
|
59
|
+
def update(doc)
|
60
|
+
r = put(doc.id, doc)
|
61
|
+
doc.send :update_rev, r[:rev]
|
62
|
+
end
|
63
|
+
|
64
|
+
def destroy(doc)
|
65
|
+
delete("#{doc.id}?rev=#{doc.rev}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
include Base
|
70
|
+
|
71
|
+
# -- document bulk methods
|
72
|
+
|
73
|
+
module Bulk
|
74
|
+
def self.included(klass)
|
75
|
+
klass.send :protected, :do_bulked
|
76
|
+
end
|
77
|
+
|
78
|
+
def create(doc)
|
79
|
+
return super unless @bulk
|
80
|
+
|
81
|
+
raise unless doc.new_record?
|
82
|
+
doc.id ||= UUID.new.generate
|
83
|
+
|
84
|
+
@bulk << doc
|
85
|
+
end
|
86
|
+
|
87
|
+
def read(id)
|
88
|
+
flush
|
89
|
+
super
|
90
|
+
end
|
91
|
+
|
92
|
+
def update(doc)
|
93
|
+
flush
|
94
|
+
super
|
95
|
+
end
|
96
|
+
|
97
|
+
def destroy(doc)
|
98
|
+
flush
|
99
|
+
super
|
100
|
+
end
|
101
|
+
|
102
|
+
def async(&block)
|
103
|
+
if @bulk
|
104
|
+
yield
|
105
|
+
else
|
106
|
+
do_bulked(&block)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def do_bulked(&block)
|
111
|
+
@bulk = []
|
112
|
+
yield
|
113
|
+
flush
|
114
|
+
ensure
|
115
|
+
@bulk = nil
|
116
|
+
end
|
117
|
+
|
118
|
+
def flush
|
119
|
+
return unless @bulk
|
120
|
+
bulk = @bulk
|
121
|
+
@bulk = nil
|
122
|
+
|
123
|
+
docs_by_id = {}
|
124
|
+
bulk.each do |doc|
|
125
|
+
docs_by_id[doc.id] = doc
|
126
|
+
end
|
127
|
+
|
128
|
+
post("_bulk_docs", :docs => bulk).
|
129
|
+
each do |rev_data|
|
130
|
+
id, rev = rev_data[:id], rev_data[:rev]
|
131
|
+
docs_by_id[id].send :update_rev, rev
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
include Bulk
|
137
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
class Couch::DocView < Couch::View
|
2
|
+
def self.find_or_create(klass, opts)
|
3
|
+
name = name_for(klass, opts)
|
4
|
+
|
5
|
+
database.memoized_views[name] ||= begin
|
6
|
+
Couch::DocView.find(name) ||
|
7
|
+
Couch::DocView.create(klass, name, opts)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.create(klass, name, opts = {})
|
12
|
+
map = <<-JS
|
13
|
+
function(doc) {
|
14
|
+
if(doc.type == #{klass.name.to_json}) { emit(#{key_expression(opts[:keys])}, 1); }
|
15
|
+
}
|
16
|
+
JS
|
17
|
+
|
18
|
+
reduce = "_sum" if opts[:mode] == :count
|
19
|
+
|
20
|
+
super name, :map => map, :reduce => reduce
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def self.name_for(klass, opts)
|
26
|
+
mode = opts[:mode]
|
27
|
+
keys = opts[:keys]
|
28
|
+
|
29
|
+
name = klass.to_s + ":#{mode}"
|
30
|
+
return name if keys.empty?
|
31
|
+
|
32
|
+
"#{name}_by_" + keys.map(&:to_s).join("_")
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.key_expression(keys)
|
36
|
+
case keys.length
|
37
|
+
when 0 then 1
|
38
|
+
when 1 then "doc.#{keys.first}"
|
39
|
+
else "[" + keys.map {|k| "doc.#{k}"}.join(", ") + "]"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Couch::DocExtensions
|
45
|
+
def view(mode, *keys)
|
46
|
+
Couch::DocView.find_or_create(self, :mode => mode, :keys => keys)
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# conditions might be empty, or might contain
|
51
|
+
# :key => required value(s) entries
|
52
|
+
def count(conditions = {})
|
53
|
+
case conditions.length
|
54
|
+
when 0
|
55
|
+
r = view(:count, *conditions.keys).all
|
56
|
+
r.inject(0) do |sum, row| sum + row[:value] end
|
57
|
+
when 1
|
58
|
+
startkey = endkey = conditions.values.first
|
59
|
+
r = view(:count, *conditions.keys).all(Couch::DocExtensions.condition_keys(conditions.values.first))
|
60
|
+
r.inject(0) do |sum, row| sum + row[:value] end
|
61
|
+
else
|
62
|
+
raise "More than one key is currently not supported"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.condition_keys(v)
|
67
|
+
case v
|
68
|
+
when Range
|
69
|
+
{ :startkey => v.first, :endkey => v.last }
|
70
|
+
else
|
71
|
+
{ :startkey => v, :endkey => v }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
Couch::Document.extend Couch::DocExtensions
|
@@ -0,0 +1,140 @@
|
|
1
|
+
class Couch::Document
|
2
|
+
include Couch::Log
|
3
|
+
|
4
|
+
def self.database
|
5
|
+
Couch.database
|
6
|
+
end
|
7
|
+
|
8
|
+
def database
|
9
|
+
self.class.database
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.async(&block)
|
13
|
+
database.async(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.create(*args)
|
17
|
+
new(*args).save
|
18
|
+
end
|
19
|
+
|
20
|
+
# instantiate a Couch::Document from a Hash.
|
21
|
+
def self.instantiate(data, target = nil)
|
22
|
+
target_type_name = data[:type] || raise("Missing document type in #{data.inspect}")
|
23
|
+
|
24
|
+
if target.nil?
|
25
|
+
target_klass = target_type_name.constantize
|
26
|
+
target = target_klass.new(data)
|
27
|
+
elsif target.class.name != target_type_name
|
28
|
+
raise "Type mismatch #{target.class.name} vs #{target_type_name}"
|
29
|
+
target.update data
|
30
|
+
end
|
31
|
+
target
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.find(id)
|
35
|
+
database.read(id)
|
36
|
+
rescue Couch::Error404
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
attr :attributes
|
41
|
+
|
42
|
+
def initialize(data = {}, dont_change = true)
|
43
|
+
@attributes = data.dup
|
44
|
+
self.id = @attributes.delete(:id) if @attributes[:id]
|
45
|
+
|
46
|
+
@attributes[:type] = self.class.name
|
47
|
+
end
|
48
|
+
|
49
|
+
def dup
|
50
|
+
other = super
|
51
|
+
other.instance_variable_set "@attributes", attributes.dup
|
52
|
+
other.instance_variable_set "@original", @original.dup
|
53
|
+
other
|
54
|
+
end
|
55
|
+
|
56
|
+
def type; attributes[:type]; end
|
57
|
+
|
58
|
+
def id; @attributes[:_id]; end
|
59
|
+
def id=(id); @attributes[:_id] = id; end
|
60
|
+
|
61
|
+
def rev; @attributes[:_rev]; end
|
62
|
+
|
63
|
+
def to_hash
|
64
|
+
r = @attributes.dup
|
65
|
+
|
66
|
+
r.delete :_id
|
67
|
+
r.delete :_rev
|
68
|
+
r.delete :type
|
69
|
+
r
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
#
|
74
|
+
|
75
|
+
def new_record?
|
76
|
+
rev.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
def reload
|
80
|
+
raise if new_record?
|
81
|
+
|
82
|
+
@attributes = database.read(self.id).attributes
|
83
|
+
@original = attributes.dup
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def save
|
88
|
+
if new_record?
|
89
|
+
database.create self
|
90
|
+
elsif changed?
|
91
|
+
database.update self
|
92
|
+
end
|
93
|
+
|
94
|
+
@original = attributes.dup
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
def changed?
|
99
|
+
@original != @attributes
|
100
|
+
end
|
101
|
+
|
102
|
+
def destroy
|
103
|
+
database.destroy self
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
def inspect
|
108
|
+
"<#{self.class.name}[#{self.id}] #{attributes.inspect}>"
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def update_rev(rev)
|
114
|
+
attributes[:_rev] = rev
|
115
|
+
end
|
116
|
+
|
117
|
+
public
|
118
|
+
|
119
|
+
def to_json
|
120
|
+
attributes.to_json
|
121
|
+
end
|
122
|
+
|
123
|
+
def update(attributes)
|
124
|
+
self.attributes.update attributes
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def method_missing(sym, *args)
|
130
|
+
if block_given?
|
131
|
+
super
|
132
|
+
elsif args.length == 0 && attributes.key?(sym)
|
133
|
+
attributes[sym]
|
134
|
+
elsif args.length == 1 && sym.to_s =~ /(.+)=$/
|
135
|
+
attributes[$1.to_sym] = args.first
|
136
|
+
else
|
137
|
+
super
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
data/lib/couch/error.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
class Couch::Error < RuntimeError
|
2
|
+
def initialize(options); @options = options; end
|
3
|
+
|
4
|
+
def to_s; @options.inspect; end
|
5
|
+
def inspect; @options.inspect; end
|
6
|
+
|
7
|
+
def self.ignore(&block)
|
8
|
+
yield
|
9
|
+
rescue Couch::Error
|
10
|
+
dlog.warn $!
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Couch::Error404 < Couch::Error
|
16
|
+
end
|
17
|
+
|
18
|
+
class Couch::Error
|
19
|
+
def self.raise(options)
|
20
|
+
case options[:code].to_s
|
21
|
+
when "404"
|
22
|
+
super Couch::Error404, options
|
23
|
+
else
|
24
|
+
super Couch::Error, options
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/couch/extras.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class Object
|
2
|
+
def define_instance_method(sym, *args, &block)
|
3
|
+
if args.empty?
|
4
|
+
singleton_class.send(:define_method, sym, &block)
|
5
|
+
else
|
6
|
+
invalid_argument! if block_given?
|
7
|
+
r = args.first
|
8
|
+
singleton_class.send(:define_method, sym) do r end
|
9
|
+
end
|
10
|
+
self
|
11
|
+
end
|
12
|
+
end
|
data/lib/couch/log.rb
ADDED
data/lib/couch/server.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'yajl'
|
2
|
+
require 'yajl/json_gem'
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
class Couch::Server
|
6
|
+
include Couch::Log
|
7
|
+
|
8
|
+
def initialize(host, port)
|
9
|
+
@host, @port = host, port
|
10
|
+
end
|
11
|
+
|
12
|
+
def delete(uri); request Net::HTTP::Delete.new(uri); end
|
13
|
+
def get(uri); request Net::HTTP::Get.new(uri); end
|
14
|
+
def put(uri, json); request Net::HTTP::Put.new(uri), json; end
|
15
|
+
def post(uri, json); request Net::HTTP::Post.new(uri), json; end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def request(request, body=nil)
|
20
|
+
if body
|
21
|
+
request["content-type"] = "application/json"
|
22
|
+
request.body = body.to_json
|
23
|
+
end
|
24
|
+
|
25
|
+
r = benchslow "[#{request.method}] #{request.path}" do
|
26
|
+
Net::HTTP.start(@host, @port) { |http| http.request(request) }
|
27
|
+
end
|
28
|
+
|
29
|
+
body = Yajl::Parser.new(:symbolize_keys => true).parse(r.body)
|
30
|
+
# dlog "-->", body
|
31
|
+
|
32
|
+
return body if r.kind_of?(Net::HTTPSuccess)
|
33
|
+
|
34
|
+
Couch::Error.raise :code => r.code,
|
35
|
+
:message => r.message,
|
36
|
+
:method => request.method,
|
37
|
+
:uri => request.path,
|
38
|
+
:body => body
|
39
|
+
end
|
40
|
+
end
|
data/lib/couch/view.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
class Couch::View < Couch::Document
|
2
|
+
DUMMY_NAME = "foo"
|
3
|
+
|
4
|
+
def self.find(name)
|
5
|
+
super("_design/#{name}")
|
6
|
+
end
|
7
|
+
|
8
|
+
# -- create a view --------------------------------------------------
|
9
|
+
|
10
|
+
def self.create(name, opts)
|
11
|
+
map = opts[:map] || raise("Missing map function")
|
12
|
+
reduce = opts[:reduce]
|
13
|
+
|
14
|
+
super :id => "_design/#{name}",
|
15
|
+
:views => { DUMMY_NAME => {
|
16
|
+
:map => map,
|
17
|
+
:reduce => reduce
|
18
|
+
} }
|
19
|
+
end
|
20
|
+
|
21
|
+
def map
|
22
|
+
view[:map]
|
23
|
+
end
|
24
|
+
|
25
|
+
def map=(map)
|
26
|
+
update :views => { DUMMY_NAME => {
|
27
|
+
:map => map,
|
28
|
+
:reduce => reduce
|
29
|
+
} }
|
30
|
+
end
|
31
|
+
|
32
|
+
def reduce
|
33
|
+
view[:reduce]
|
34
|
+
end
|
35
|
+
|
36
|
+
def reduce=(reduce)
|
37
|
+
update :views => { DUMMY_NAME => {
|
38
|
+
:map => map,
|
39
|
+
:reduce => reduce
|
40
|
+
} }
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def view
|
46
|
+
views[DUMMY_NAME]
|
47
|
+
end
|
48
|
+
|
49
|
+
# -- query the view -------------------------------------------------
|
50
|
+
|
51
|
+
public
|
52
|
+
|
53
|
+
def all(opts = {})
|
54
|
+
opts = opts.to_a.inject({}) do |h, (k,v)|
|
55
|
+
h.update k => Yajl::Encoder.encode(v)
|
56
|
+
end
|
57
|
+
|
58
|
+
r = Couch.database.get CGI.url_for("#{id}/_view/#{DUMMY_NAME}", opts)
|
59
|
+
r[:rows].
|
60
|
+
define_instance_method(:offset, r[:offset]).
|
61
|
+
define_instance_method(:total_rows, r[:total_rows])
|
62
|
+
end
|
63
|
+
|
64
|
+
def total_rows(opts = nil)
|
65
|
+
if opts.nil?
|
66
|
+
r = Couch.database.get CGI.url_for("#{id}/_view/#{DUMMY_NAME}")
|
67
|
+
r[:total_rows]
|
68
|
+
else
|
69
|
+
all(opts).length
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Start a console that initializes the gem
|
4
|
+
#
|
5
|
+
require "irb"
|
6
|
+
require "rubygems"
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'wirble'
|
10
|
+
Wirble.init
|
11
|
+
Wirble.colorize
|
12
|
+
rescue LoadError
|
13
|
+
STDERR.puts "To enable colorized and tab completed run 'gem install wirble'"
|
14
|
+
end
|
15
|
+
|
16
|
+
$: << "#{File.dirname(__FILE__)}/../lib"
|
17
|
+
require "#{File.dirname(__FILE__)}/../init.rb"
|
18
|
+
|
19
|
+
IRB.start
|
data/script/rebuild
ADDED
data/script/watchr
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'fsevent' # This comes from ruby-fsevent
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
case ARGV.length
|
7
|
+
when 0
|
8
|
+
if File.exists?(".watchr.yml")
|
9
|
+
STDERR.puts "Reloading config"
|
10
|
+
args = YAML.load File.read(".watchr.yml")
|
11
|
+
end
|
12
|
+
when 1
|
13
|
+
nil
|
14
|
+
else
|
15
|
+
File.open(".watchr.yml", "w") do |f|
|
16
|
+
f.write ARGV.to_yaml
|
17
|
+
end
|
18
|
+
args = ARGV
|
19
|
+
end
|
20
|
+
|
21
|
+
if !args
|
22
|
+
STDERR.puts "watchr dir ruby-parameter ruby-parameter ruby-parameter ..."
|
23
|
+
abort
|
24
|
+
end
|
25
|
+
|
26
|
+
dirs = args.shift.split(",")
|
27
|
+
$args = args.dup
|
28
|
+
|
29
|
+
STDERR.puts "Watching #{dirs.join(",")}"
|
30
|
+
|
31
|
+
class PrintChange < FSEvent
|
32
|
+
def on_change(directories)
|
33
|
+
if !directories
|
34
|
+
STDERR.print "Initial run: "
|
35
|
+
else
|
36
|
+
STDERR.print "\n\n\nDetected change in: #{directories.first}: "
|
37
|
+
end
|
38
|
+
|
39
|
+
STDERR.puts $args.join(" ")
|
40
|
+
STDERR.puts "=" * 80
|
41
|
+
system *$args
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
printer = PrintChange.new
|
46
|
+
printer.on_change nil
|
47
|
+
printer.watch_directories dirs
|
48
|
+
printer.start
|
data/test/couch.json
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
load File.dirname(__FILE__) + "/../couch_test_helper.rb"
|
2
|
+
|
3
|
+
class BasicTest < Couch::TestCase
|
4
|
+
|
5
|
+
def test_create
|
6
|
+
post = Post.create(:body => "Post #1")
|
7
|
+
assert_equal({:body => "Post #1"}, post.to_hash)
|
8
|
+
assert !post.new_record?
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_attributes
|
12
|
+
post = Post.new(:body => "Post #1")
|
13
|
+
|
14
|
+
assert_equal("Post #1", post.body)
|
15
|
+
post.body = "body"
|
16
|
+
assert_equal("body", post.body)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_basic
|
20
|
+
post = Post.new(:body => "Post #1")
|
21
|
+
assert_equal({:body => "Post #1"}, post.to_hash)
|
22
|
+
assert post.new_record?
|
23
|
+
|
24
|
+
post.save
|
25
|
+
assert_equal("Post #1", post.body)
|
26
|
+
|
27
|
+
assert_not_nil(post.rev)
|
28
|
+
assert !post.new_record?
|
29
|
+
|
30
|
+
dup = post.dup
|
31
|
+
|
32
|
+
rev1 = post.rev
|
33
|
+
post.save
|
34
|
+
assert_equal(rev1, post.rev)
|
35
|
+
|
36
|
+
post.update(:body => "Post #2")
|
37
|
+
assert !post.new_record?
|
38
|
+
post.save
|
39
|
+
assert_not_equal(rev1, post.rev)
|
40
|
+
|
41
|
+
post.reload
|
42
|
+
assert_equal(post.to_hash, { :body => "Post #2" })
|
43
|
+
|
44
|
+
assert_equal(dup.to_hash, { :body => "Post #1" })
|
45
|
+
assert_equal(rev1, dup.rev)
|
46
|
+
|
47
|
+
assert_equal({ :body => "Post #1" }, dup.to_hash)
|
48
|
+
dup.reload
|
49
|
+
assert_equal({ :body => "Post #2" }, dup.to_hash)
|
50
|
+
assert_equal(post.rev, dup.rev)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_named_object
|
54
|
+
post = Post.new(:id => "test", :body => "view")
|
55
|
+
assert_equal("test", post.id)
|
56
|
+
post.save
|
57
|
+
assert_equal("test", post.id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_named_object_w_create
|
61
|
+
post = Post.create(:id => "test", :body => "view")
|
62
|
+
assert_equal("test", post.id)
|
63
|
+
post.save
|
64
|
+
assert_equal("test", post.id)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
load File.dirname(__FILE__) + "/../couch_test_helper.rb"
|
2
|
+
|
3
|
+
class BulkTest < Couch::TestCase
|
4
|
+
attr :view
|
5
|
+
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
|
9
|
+
@view = Couch::View.create "types", :map => MAP_ONE
|
10
|
+
|
11
|
+
assert_equal(0, view.total_rows)
|
12
|
+
assert_equal(0, view.all.offset)
|
13
|
+
end
|
14
|
+
|
15
|
+
def do_test_cnt(cnt)
|
16
|
+
post = nil
|
17
|
+
|
18
|
+
benchmark "=== writing #{cnt} objects" do
|
19
|
+
Post.async do
|
20
|
+
(1..cnt).each do |i| post = Post.create(:body => "Post ##{i}") end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
assert !post.new_record?
|
25
|
+
|
26
|
+
benchmark("Querying view") do assert_equal(cnt, view.total_rows) end
|
27
|
+
benchmark("Requerying view") do assert_equal(cnt, view.total_rows) end
|
28
|
+
|
29
|
+
view.map = MAP_TWICE
|
30
|
+
view.save
|
31
|
+
|
32
|
+
benchmark("Querying view") do assert_equal(2 * cnt, view.total_rows) end
|
33
|
+
benchmark("Requerying view") do assert_equal(2 * cnt, view.total_rows) end
|
34
|
+
|
35
|
+
post.destroy
|
36
|
+
|
37
|
+
benchmark("Requerying view") do assert_equal(2 * (cnt - 1), view.total_rows) end
|
38
|
+
|
39
|
+
benchmark("Count via Post.count") do assert_equal(cnt - 1, Post.count) end
|
40
|
+
benchmark("Recount via Post.count") do assert_equal(cnt - 1, Post.count) end
|
41
|
+
end
|
42
|
+
|
43
|
+
COUNTS = [ 10, 100, 1000, 10000, 100000 ]
|
44
|
+
COUNTS = [ 10, 100 ]
|
45
|
+
|
46
|
+
COUNTS.each do |i|
|
47
|
+
define_method "test_view_#{i}" do
|
48
|
+
do_test_cnt i
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
load File.dirname(__FILE__) + "/../couch_test_helper.rb"
|
2
|
+
|
3
|
+
class DocViewTest < Couch::TestCase
|
4
|
+
def test_wo_args
|
5
|
+
p1 = Post.create(:body => "Post ##{1}")
|
6
|
+
assert_equal(1, Post.count)
|
7
|
+
|
8
|
+
p2 = Post.create(:body => "Post ##{2}")
|
9
|
+
assert_equal(2, Post.count)
|
10
|
+
|
11
|
+
p3 = Post.create(:body => "Post ##{3}")
|
12
|
+
assert_equal(3, Post.count)
|
13
|
+
|
14
|
+
p3.destroy
|
15
|
+
assert_equal(2, Post.count)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_with_one_arg
|
19
|
+
Post.create(:body => "Post ##{1}", :number => 1)
|
20
|
+
assert_equal(0, Post.count(:number => 0))
|
21
|
+
|
22
|
+
Post.create(:body => "Post ##{1}", :number => 1)
|
23
|
+
|
24
|
+
assert_equal(0, Post.count(:number => 0))
|
25
|
+
assert_equal(2, Post.count(:number => 1))
|
26
|
+
assert_equal(0, Post.count(:number => 2))
|
27
|
+
|
28
|
+
|
29
|
+
p2 = Post.create(:body => "Post ##{2}", :number => 2)
|
30
|
+
|
31
|
+
assert_equal(0, Post.count(:number => 0))
|
32
|
+
assert_equal(2, Post.count(:number => 1))
|
33
|
+
assert_equal(1, Post.count(:number => 2))
|
34
|
+
|
35
|
+
p3 = Post.create(:body => "Post ##{3}", :number => 1)
|
36
|
+
assert_equal(0, Post.count(:number => 0))
|
37
|
+
assert_equal(3, Post.count(:number => 1))
|
38
|
+
assert_equal(1, Post.count(:number => 2))
|
39
|
+
|
40
|
+
p2.destroy
|
41
|
+
assert_equal(0, Post.count(:number => 0))
|
42
|
+
assert_equal(3, Post.count(:number => 1))
|
43
|
+
assert_equal(0, Post.count(:number => 2))
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_with_range_condition
|
47
|
+
Post.create(:body => "Post ##{1}", :number => 1)
|
48
|
+
Post.create(:body => "Post ##{1}", :number => 1)
|
49
|
+
Post.create(:body => "Post ##{1}", :number => 2)
|
50
|
+
|
51
|
+
assert_equal(0, Post.count(:number => 0..0.5))
|
52
|
+
assert_equal(2, Post.count(:number => -1..1))
|
53
|
+
assert_equal(3, Post.count(:number => -3..100))
|
54
|
+
end
|
55
|
+
end
|
data/test/couch/post.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
load File.dirname(__FILE__) + "/../couch_test_helper.rb"
|
2
|
+
|
3
|
+
class ViewTest < Couch::TestCase
|
4
|
+
def test_none
|
5
|
+
view = Couch::View.create "none", :map => MAP_NONE
|
6
|
+
assert_equal(0, view.total_rows)
|
7
|
+
Post.create(:body => "Post ##{1}")
|
8
|
+
assert_equal(0, view.total_rows)
|
9
|
+
|
10
|
+
view.map = MAP_ONE
|
11
|
+
view.save
|
12
|
+
assert_equal(1, view.total_rows)
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_one
|
16
|
+
view = Couch::View.create "to_self", :map => MAP_ONE
|
17
|
+
assert_equal(0, view.total_rows)
|
18
|
+
Post.create(:body => "Post ##{1}")
|
19
|
+
assert_equal(1, view.total_rows)
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_start_end_key
|
23
|
+
view = Couch::View.create "to_self", :map => MAP_ONE
|
24
|
+
assert_equal(0, view.total_rows)
|
25
|
+
Post.create(:body => "Post ##{1}")
|
26
|
+
assert_equal(1, view.total_rows)
|
27
|
+
|
28
|
+
assert_equal(1, view.total_rows(:startkey => "p", :endkey => "q"))
|
29
|
+
assert_equal(0, view.total_rows(:startkey => "q", :endkey => "r"))
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_view
|
33
|
+
view = Couch::View.create "types", :map => MAP_ONE
|
34
|
+
|
35
|
+
assert_equal(0, view.total_rows)
|
36
|
+
assert_equal(0, view.all.offset)
|
37
|
+
|
38
|
+
post = nil
|
39
|
+
|
40
|
+
benchmark "writing #{COUNT} objects" do
|
41
|
+
(1..COUNT).each do |i|
|
42
|
+
post = Post.create(:body => "Post ##{i}")
|
43
|
+
assert !post.new_record?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
assert_equal(COUNT, view.total_rows)
|
48
|
+
assert_equal(COUNT, view.total_rows)
|
49
|
+
|
50
|
+
view.map = MAP_TWICE
|
51
|
+
view.save
|
52
|
+
|
53
|
+
assert_equal(2 * COUNT, view.total_rows)
|
54
|
+
assert_equal(2 * COUNT, view.total_rows)
|
55
|
+
|
56
|
+
post.destroy
|
57
|
+
|
58
|
+
assert_equal(2 * (COUNT - 1), view.total_rows)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
unless defined?(Couch::TestCase)
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + "/test_helper"
|
4
|
+
require File.dirname(__FILE__) + "/../lib/couch"
|
5
|
+
|
6
|
+
require File.dirname(__FILE__) + "/couch/post"
|
7
|
+
|
8
|
+
CONFIG = JSON.parse(File.read(File.dirname(__FILE__) + "/couch.json"))
|
9
|
+
dlog "Using couchdb config", CONFIG
|
10
|
+
|
11
|
+
class Couch::TestCase < BaseTestCase
|
12
|
+
MAP_ONE = "function(post) { if(post.type) { emit(post.type); } }"
|
13
|
+
MAP_TWICE = "function(post) { if(post.type) { emit(1); emit(2); } }"
|
14
|
+
MAP_NONE = "function(post) { }"
|
15
|
+
|
16
|
+
COUNT = 10
|
17
|
+
|
18
|
+
def setup
|
19
|
+
host = CONFIG["host"]
|
20
|
+
port = CONFIG["port"] || 5984
|
21
|
+
database = CONFIG["database"]
|
22
|
+
|
23
|
+
Couch.connect(host, port, database, true)
|
24
|
+
end
|
25
|
+
|
26
|
+
def teardown
|
27
|
+
Couch.disconnect
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
data/test/test.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
unless defined?(BaseTestCase)
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
require "ruby-debug"
|
5
|
+
|
6
|
+
# Use test/unit
|
7
|
+
#
|
8
|
+
# require "test/unit"
|
9
|
+
#
|
10
|
+
# class BaseTestCase < Test::Unit::TestCase
|
11
|
+
# def test_dummy
|
12
|
+
# # or else test unit cries out loud-
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
|
16
|
+
require "minitest/unit"
|
17
|
+
require 'minitest/autorun'
|
18
|
+
|
19
|
+
class BaseTestCase < MiniTest::Unit::TestCase
|
20
|
+
def assert_not_nil(object, message = nil)
|
21
|
+
assert !object.nil?, message || "Should not be nil"
|
22
|
+
end
|
23
|
+
|
24
|
+
def assert_not_equal(unexpected, actual, message = nil)
|
25
|
+
assert unexpected != actual, message || "#{actual.inspect} should not equal #{unexpected.inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
data/vex/gem.rake
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
def sys(*args)
|
2
|
+
STDERR.puts "#{args.join(" ")}"
|
3
|
+
system *args
|
4
|
+
end
|
5
|
+
|
6
|
+
namespace :gem do
|
7
|
+
task :build => :spec do
|
8
|
+
options = []
|
9
|
+
sys "gem build .gemspec #{options.join(" ")} && mkdir -p pkg && mv *.gem pkg"
|
10
|
+
end
|
11
|
+
|
12
|
+
task :spec do
|
13
|
+
File.open ".gemspec", "w" do |file|
|
14
|
+
file.write <<-TXT
|
15
|
+
require "vex/gem"
|
16
|
+
Gem.spec File.dirname(__FILE__)
|
17
|
+
TXT
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
task :install => :build do
|
22
|
+
file = Dir.glob("pkg/*.gem").sort.last
|
23
|
+
sys "gem install #{file}"
|
24
|
+
end
|
25
|
+
|
26
|
+
task :push => :build do
|
27
|
+
file = Dir.glob("pkg/*.gem").sort.last
|
28
|
+
puts "To push the gem to gemcutter please run"
|
29
|
+
puts
|
30
|
+
puts "\tgem push #{file}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "Build gem"
|
35
|
+
# task :gem => %w(test gem:install)
|
36
|
+
task :gem => %w(gem:install gem:push)
|
data/vex/gem.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
module Gem
|
2
|
+
(class << self; self; end).class_eval do
|
3
|
+
def default_attributes
|
4
|
+
%w(name version date files executables)
|
5
|
+
end
|
6
|
+
|
7
|
+
def set_root(root)
|
8
|
+
@root = File.expand_path(root)
|
9
|
+
@name = File.basename(@root)
|
10
|
+
@conf ||= YAML.load File.read("#{@root}/gem.yml")
|
11
|
+
@attributes ||= (default_attributes + @conf.keys.map(&:to_s)).uniq
|
12
|
+
end
|
13
|
+
|
14
|
+
# attr_reader :root, :conf, :attributes,
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
def attribute(name)
|
18
|
+
if @conf.key?(name) && !%w(dependencies).include?(name)
|
19
|
+
@conf[name]
|
20
|
+
else
|
21
|
+
self.send(name)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# create a new Gem::Specification object for this gem.
|
27
|
+
def spec(root)
|
28
|
+
self.set_root root
|
29
|
+
|
30
|
+
Gem::Specification.new do |s|
|
31
|
+
@attributes.each do |attr|
|
32
|
+
v = attribute(attr)
|
33
|
+
next if v.nil?
|
34
|
+
|
35
|
+
log attr, v
|
36
|
+
|
37
|
+
s.send attr + "=", v
|
38
|
+
end
|
39
|
+
|
40
|
+
%w(pre_uninstall post_install).each do |hook|
|
41
|
+
next unless File.exists?("#{root}/hooks/#{hook}.rb")
|
42
|
+
log hook, "yes"
|
43
|
+
Gem.send(hook) {
|
44
|
+
load "hooks/#{hook}.rb"
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def log(attr, v)
|
51
|
+
v = case attr
|
52
|
+
when "files" then "#{v.length} files"
|
53
|
+
else v.inspect
|
54
|
+
end
|
55
|
+
|
56
|
+
STDERR.puts "#{"%20s" % attr}:\t#{v}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def dependencies
|
60
|
+
return nil unless @conf["dependencies"]
|
61
|
+
|
62
|
+
@conf["dependencies"].map do |d|
|
63
|
+
Gem::Dependency.new d, ">= 0"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def head(file)
|
68
|
+
File.open(file) do |f|
|
69
|
+
f.readline
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def executables
|
74
|
+
r = Dir.glob("#{@root}/bin/**/*").map do |file|
|
75
|
+
next unless head(file) =~ /^#!/
|
76
|
+
file[@root.length + 5 .. -1]
|
77
|
+
end.compact
|
78
|
+
|
79
|
+
return nil if r.empty?
|
80
|
+
r
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# get files from git
|
85
|
+
def files
|
86
|
+
r = `git ls-files`.split("\n")
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# return the date
|
91
|
+
def date
|
92
|
+
Date.today.to_s
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dispaaro-couch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- radiospiel
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-12-08 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: etest
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
description: Yet another CouchDB handler
|
35
|
+
email: eno@open-lab.org
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- Rakefile
|
45
|
+
- gem.yml
|
46
|
+
- lib/couch.rb
|
47
|
+
- lib/couch/database.rb
|
48
|
+
- lib/couch/doc_view.rb
|
49
|
+
- lib/couch/document.rb
|
50
|
+
- lib/couch/error.rb
|
51
|
+
- lib/couch/extras.rb
|
52
|
+
- lib/couch/log.rb
|
53
|
+
- lib/couch/server.rb
|
54
|
+
- lib/couch/view.rb
|
55
|
+
- script/console
|
56
|
+
- script/rebuild
|
57
|
+
- script/watchr
|
58
|
+
- test/couch.json
|
59
|
+
- test/couch/basic_test.rb
|
60
|
+
- test/couch/bulk_test.rb
|
61
|
+
- test/couch/doc_view_test.rb
|
62
|
+
- test/couch/post.rb
|
63
|
+
- test/couch/view_test.rb
|
64
|
+
- test/couch_test_helper.rb
|
65
|
+
- test/test.rb
|
66
|
+
- test/test_helper.rb
|
67
|
+
- vex/gem.rake
|
68
|
+
- vex/gem.rb
|
69
|
+
has_rdoc: true
|
70
|
+
homepage: http://github.com/radiospiel/dispaaro-couch
|
71
|
+
licenses: []
|
72
|
+
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
hash: 3
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
hash: 3
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
version: "0"
|
96
|
+
requirements: []
|
97
|
+
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.3.7
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: A CouchDB handler
|
103
|
+
test_files: []
|
104
|
+
|