rocking_chair 0.0.2
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/lib/rocking_chair.rb +33 -0
- data/lib/rocking_chair/couch_rest_http_adapter.rb +80 -0
- data/lib/rocking_chair/database.rb +176 -0
- data/lib/rocking_chair/error.rb +44 -0
- data/lib/rocking_chair/helper.rb +15 -0
- data/lib/rocking_chair/server.rb +137 -0
- data/lib/rocking_chair/view.rb +284 -0
- data/test/couch_rest_test.rb +350 -0
- data/test/database_test.rb +447 -0
- data/test/extended_couch_rest_test.rb +49 -0
- data/test/fixtures/extended_couch_rest_fixtures.rb +23 -0
- data/test/fixtures/simply_stored_fixtures.rb +36 -0
- data/test/simply_stored_test.rb +214 -0
- data/test/test_helper.rb +36 -0
- data/test/view_test.rb +372 -0
- metadata +77 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "active_support"
|
3
|
+
require "uuidtools"
|
4
|
+
require "cgi"
|
5
|
+
require "couchrest"
|
6
|
+
|
7
|
+
|
8
|
+
require "rocking_chair/helper"
|
9
|
+
require "rocking_chair/error"
|
10
|
+
require "rocking_chair/view"
|
11
|
+
require "rocking_chair/database"
|
12
|
+
require "rocking_chair/server"
|
13
|
+
require "rocking_chair/couch_rest_http_adapter"
|
14
|
+
|
15
|
+
module RockingChair
|
16
|
+
|
17
|
+
@_rocking_chair_enabled
|
18
|
+
|
19
|
+
def self.enable
|
20
|
+
unless @_rocking_chair_enabled
|
21
|
+
HttpAbstraction.extend(RockingChair::CouchRestHttpAdapter)
|
22
|
+
@_rocking_chair_enabled = true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.disable
|
27
|
+
if @_rocking_chair_enabled
|
28
|
+
HttpAbstraction.extend(RestClientAdapter::API)
|
29
|
+
@_rocking_chair_enabled = false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module RockingChair
|
2
|
+
module CouchRestHttpAdapter
|
3
|
+
URL_PARAMETER = /[a-zA-Z0-9\-\_\%]+/
|
4
|
+
|
5
|
+
@_RockingChair_debug = true
|
6
|
+
|
7
|
+
def get(uri, headers={})
|
8
|
+
puts "GET: #{uri.inspect}: #{headers.inspect}" if @_RockingChair_debug
|
9
|
+
url, parameters = RockingChair::Server.normalize_url(uri)
|
10
|
+
if url == ''
|
11
|
+
RockingChair::Server.info
|
12
|
+
elsif url == '_all_dbs'
|
13
|
+
RockingChair::Server.all_dbs(parameters)
|
14
|
+
elsif url == '_uuids'
|
15
|
+
RockingChair::Server.uuids(parameters)
|
16
|
+
elsif url.match(/\A(#{URL_PARAMETER})\Z/)
|
17
|
+
RockingChair::Server.database($1, parameters)
|
18
|
+
elsif url.match(/\A(#{URL_PARAMETER})\/_all_docs\Z/)
|
19
|
+
RockingChair::Server.load_all($1, parameters)
|
20
|
+
elsif url.match(/\A(#{URL_PARAMETER})\/(#{URL_PARAMETER})\Z/)
|
21
|
+
RockingChair::Server.load($1, $2, parameters)
|
22
|
+
elsif url.match(/\A(#{URL_PARAMETER})\/_design\/(#{URL_PARAMETER})\Z/)
|
23
|
+
RockingChair::Server.load($1, "_design/#{$2}", parameters)
|
24
|
+
elsif url.match(/\A(#{URL_PARAMETER})\/_design\/(#{URL_PARAMETER})\/_view\/(#{URL_PARAMETER})\Z/)
|
25
|
+
RockingChair::Server.view($1, $2, $3, parameters)
|
26
|
+
else
|
27
|
+
raise "GET: Unknown url: #{url.inspect} headers: #{headers.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def post(uri, payload, headers={})
|
32
|
+
puts "POST: #{uri.inspect}: #{payload.inspect} #{headers.inspect}" if @_RockingChair_debug
|
33
|
+
url, parameters = RockingChair::Server.normalize_url(uri)
|
34
|
+
if url.match(/\A(#{URL_PARAMETER})\/?\Z/)
|
35
|
+
RockingChair::Server.store($1, nil, payload, parameters)
|
36
|
+
elsif url.match(/\A(#{URL_PARAMETER})\/(#{URL_PARAMETER})\Z/) && $2 == '_bulk_docs'
|
37
|
+
RockingChair::Server.bulk($1, payload)
|
38
|
+
else
|
39
|
+
raise "POST: Unknown url: #{uri.inspect}: #{payload.inspect} #{headers.inspect}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def put(uri, payload, headers={})
|
44
|
+
puts "PUT: #{uri.inspect}: #{payload.inspect} #{headers.inspect}" if @_RockingChair_debug
|
45
|
+
url, parameters = RockingChair::Server.normalize_url(uri)
|
46
|
+
if url.match(/\A(#{URL_PARAMETER})\Z/)
|
47
|
+
RockingChair::Server.create_db(url)
|
48
|
+
elsif url.match(/\A(#{URL_PARAMETER})\/(#{URL_PARAMETER})\Z/)
|
49
|
+
RockingChair::Server.store($1, $2, payload, parameters)
|
50
|
+
elsif url.match(/\A(#{URL_PARAMETER})\/_design\/(#{URL_PARAMETER})\Z/)
|
51
|
+
RockingChair::Server.store($1, "_design/#{$2}", payload, parameters)
|
52
|
+
else
|
53
|
+
raise "PUT: Unknown url: #{uri.inspect}: #{payload.inspect} #{headers.inspect}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete(uri, headers={})
|
58
|
+
puts "DELETE: #{uri.inspect}: #{headers.inspect}" if @_RockingChair_debug
|
59
|
+
url, parameters = RockingChair::Server.normalize_url(uri)
|
60
|
+
if url.match(/\A(#{URL_PARAMETER})\Z/)
|
61
|
+
RockingChair::Server.delete_db(url)
|
62
|
+
elsif url.match(/\A(#{URL_PARAMETER})\/(#{URL_PARAMETER})\Z/)
|
63
|
+
RockingChair::Server.delete($1, $2, parameters)
|
64
|
+
else
|
65
|
+
raise "DELETE: Unknown url: #{uri.inspect}: #{headers.inspect}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def copy(uri, headers)
|
70
|
+
puts "COPY: #{uri.inspect}: #{headers.inspect}" if @_RockingChair_debug
|
71
|
+
url, parameters = RockingChair::Server.normalize_url(uri)
|
72
|
+
if url.match(/\A(#{URL_PARAMETER})\/(#{URL_PARAMETER})\Z/)
|
73
|
+
RockingChair::Server.copy($1, $2, headers.merge(parameters))
|
74
|
+
else
|
75
|
+
raise "COPY: Unknown url: #{uri.inspect}: #{headers.inspect}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module RockingChair
|
2
|
+
class Database
|
3
|
+
|
4
|
+
attr_accessor :storage
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@storage = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.uuid
|
11
|
+
UUIDTools::UUID.random_create().to_s.gsub('-', '')
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.uuids(count)
|
15
|
+
ids = []
|
16
|
+
count.to_i.times {ids << uuid}
|
17
|
+
ids
|
18
|
+
end
|
19
|
+
|
20
|
+
def rev
|
21
|
+
self.class.uuid
|
22
|
+
end
|
23
|
+
|
24
|
+
def exists?(doc_id)
|
25
|
+
storage.has_key?(doc_id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](doc_id)
|
29
|
+
if exists?(doc_id)
|
30
|
+
return storage[doc_id]
|
31
|
+
else
|
32
|
+
RockingChair::Error.raise_404
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def load(doc_id, options = {})
|
37
|
+
options = {
|
38
|
+
'rev' => nil,
|
39
|
+
'revs' => false
|
40
|
+
}.update(options)
|
41
|
+
options.assert_valid_keys('rev', 'revs', 'revs_info')
|
42
|
+
|
43
|
+
document = self[doc_id]
|
44
|
+
if options['rev'] && ( ActiveSupport::JSON.decode(document)['_rev'] != options['rev'])
|
45
|
+
RockingChair::Error.raise_404
|
46
|
+
end
|
47
|
+
if options['revs'] && options['revs'] == 'true'
|
48
|
+
json = ActiveSupport::JSON.decode(document)
|
49
|
+
json['_revisions'] = {'start' => 1, 'ids' => [json['_rev']]}
|
50
|
+
document = json.to_json
|
51
|
+
end
|
52
|
+
if options['revs_info'] && options['revs_info'] == 'true'
|
53
|
+
json = ActiveSupport::JSON.decode(document)
|
54
|
+
json['_revs_info'] = [{"rev" => json['_rev'], "status" => "disk"}]
|
55
|
+
document = json.to_json
|
56
|
+
end
|
57
|
+
document
|
58
|
+
end
|
59
|
+
|
60
|
+
def []=(doc_id, document, options ={})
|
61
|
+
# options are ignored for now: batch, bulk
|
62
|
+
json = nil
|
63
|
+
begin
|
64
|
+
json = ActiveSupport::JSON.decode(document)
|
65
|
+
raise "is not a Hash" unless json.is_a?(Hash)
|
66
|
+
rescue Exception => e
|
67
|
+
raise RockingChair::Error.new(500, 'InvalidJSON', "the document is not a valid JSON object: #{e}")
|
68
|
+
end
|
69
|
+
|
70
|
+
if exists?(doc_id)
|
71
|
+
update(doc_id, json)
|
72
|
+
else
|
73
|
+
insert(doc_id, json)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
alias_method :store, :[]=
|
78
|
+
|
79
|
+
def delete(doc_id, rev)
|
80
|
+
if exists?(doc_id)
|
81
|
+
existing = self[doc_id]
|
82
|
+
if matching_revision?(existing, rev)
|
83
|
+
storage.delete(doc_id)
|
84
|
+
else
|
85
|
+
RockingChair::Error.raise_409
|
86
|
+
end
|
87
|
+
else
|
88
|
+
RockingChair::Error.raise_404
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def copy(original_id, new_id, rev=nil)
|
93
|
+
original = JSON.parse(self[original_id])
|
94
|
+
if rev
|
95
|
+
original['_rev'] = rev
|
96
|
+
else
|
97
|
+
original.delete('_rev')
|
98
|
+
end
|
99
|
+
|
100
|
+
self.store(new_id, original.to_json)
|
101
|
+
end
|
102
|
+
|
103
|
+
def bulk(documents)
|
104
|
+
documents = JSON.parse(documents)
|
105
|
+
response = []
|
106
|
+
documents['docs'].each do |doc|
|
107
|
+
begin
|
108
|
+
if exists?(doc['_id']) && doc['_deleted'].to_s == 'true'
|
109
|
+
self.delete(doc['_id'], doc['_rev'])
|
110
|
+
state = {'id' => doc['_id'], 'rev' => doc['_rev']}
|
111
|
+
else
|
112
|
+
state = JSON.parse(self.store(doc['_id'], doc.to_json))
|
113
|
+
end
|
114
|
+
response << {'id' => state['id'], 'rev' => state['rev']}
|
115
|
+
rescue RockingChair::Error => e
|
116
|
+
response << {'id' => doc['_id'], 'error' => e.error, 'reason' => e.reason}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
response.to_json
|
120
|
+
end
|
121
|
+
|
122
|
+
def document_count
|
123
|
+
storage.keys.size
|
124
|
+
end
|
125
|
+
|
126
|
+
def all_documents(options = {})
|
127
|
+
View.run_all(self, options)
|
128
|
+
end
|
129
|
+
|
130
|
+
def view(design_doc_name, view_name, options = {})
|
131
|
+
View.run(self, design_doc_name, view_name, options)
|
132
|
+
end
|
133
|
+
|
134
|
+
protected
|
135
|
+
|
136
|
+
def state_tuple(_id, _rev)
|
137
|
+
{"ok" => true, "id" => _id, "rev" => _rev }.to_json
|
138
|
+
end
|
139
|
+
|
140
|
+
def update(doc_id, json)
|
141
|
+
existing = self[doc_id]
|
142
|
+
if matching_revision?(existing, json['_rev'])
|
143
|
+
insert(doc_id, json)
|
144
|
+
else
|
145
|
+
RockingChair::Error.raise_409
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def insert(doc_id, json)
|
150
|
+
json.delete('_rev')
|
151
|
+
json.delete('_id')
|
152
|
+
json['_rev'] = rev
|
153
|
+
json['_id'] = doc_id || self.class.uuid
|
154
|
+
validate_document(json)
|
155
|
+
storage[doc_id] = json.to_json
|
156
|
+
|
157
|
+
state_tuple(json['_id'], json['_rev'])
|
158
|
+
end
|
159
|
+
|
160
|
+
def validate_document(doc)
|
161
|
+
if design_doc?(doc)
|
162
|
+
RockingChair::Error.raise_500 unless doc['views'].is_a?(Hash)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def design_doc?(doc)
|
167
|
+
doc['_id'] && doc['_id'].match(/_design\/[a-zA-Z0-9\_\-]+/)
|
168
|
+
end
|
169
|
+
|
170
|
+
def matching_revision?(existing_record, rev)
|
171
|
+
document = JSON.parse(existing_record)
|
172
|
+
RockingChair::Helper.access('_rev', document) == rev
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module RockingChair
|
2
|
+
class Error < StandardError
|
3
|
+
|
4
|
+
attr_reader :code, :error, :reason
|
5
|
+
|
6
|
+
def initialize(code, error, reason)
|
7
|
+
@code = code
|
8
|
+
@error = error
|
9
|
+
@reason = reason
|
10
|
+
end
|
11
|
+
|
12
|
+
def message
|
13
|
+
"#{@code} - #{@error} - #{@reason}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_json
|
17
|
+
{"error" => @error, "reason" => @reason }.to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
def raise_rest_client_error
|
21
|
+
case code
|
22
|
+
when 404
|
23
|
+
raise RestClient::ResourceNotFound
|
24
|
+
when 409
|
25
|
+
raise HttpAbstraction::Conflict
|
26
|
+
else
|
27
|
+
raise "Unknown error code: #{code.inspect}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.raise_404
|
32
|
+
raise RockingChair::Error.new(404, 'not_found', "missing")
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.raise_409
|
36
|
+
raise RockingChair::Error.new(409, 'conflict', "Document update conflict.")
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.raise_500
|
40
|
+
raise RockingChair::Error.new(500, 'invalid', "the document is invalid.")
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module RockingChair
|
2
|
+
module Helper
|
3
|
+
|
4
|
+
def self.jsonfy_options(options, *keys)
|
5
|
+
keys.each do |key|
|
6
|
+
options[key] = ActiveSupport::JSON.decode(options[key]) if options[key]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.access(attr_name, doc)
|
11
|
+
doc.respond_to?(:_document) ? doc._document[attr_name] : doc[attr_name]
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module RockingChair
|
2
|
+
module Server
|
3
|
+
BASE_URL = /(http:\/\/)?127\.0\.0\.1:5984\//
|
4
|
+
|
5
|
+
def self.databases
|
6
|
+
@databases ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.reset
|
10
|
+
@databases = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.normalize_options(options)
|
14
|
+
(options || {}).each do |k,v|
|
15
|
+
options[k] = options[k].first if options[k].is_a?(Array)
|
16
|
+
end
|
17
|
+
options
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.normalize_url(url)
|
21
|
+
url = url.to_s.gsub(BASE_URL, '') #.gsub('%2F', '/')
|
22
|
+
if url.match(/\A(.*)\?(.*)?\Z/)
|
23
|
+
return [$1, normalize_options(CGI::parse($2 || ''))]
|
24
|
+
else
|
25
|
+
return [url, {}]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.info
|
30
|
+
respond_with({"couchdb" => "Welcome","version" => "0.10.1"})
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.all_dbs(options = {})
|
34
|
+
respond_with(databases.keys)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.uuids(options = {})
|
38
|
+
options = {
|
39
|
+
'count' => 100
|
40
|
+
}.update(options)
|
41
|
+
options['count'] = options['count'].first if options['count'].is_a?(Array)
|
42
|
+
|
43
|
+
respond_with({"uuids" => RockingChair::Database.uuids(options['count']) })
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.create_db(name)
|
47
|
+
databases[name] = RockingChair::Database.new
|
48
|
+
respond_with({"ok" => true})
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.delete_db(name)
|
52
|
+
databases.delete(name)
|
53
|
+
respond_with({"ok" => true})
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.store(db_name, doc_id, document, options)
|
57
|
+
return respond_with_error(404) unless databases.has_key?(db_name)
|
58
|
+
databases[db_name].store(doc_id, document, options)
|
59
|
+
rescue RockingChair::Error => e
|
60
|
+
e.raise_rest_client_error
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.delete(db_name, doc_id, options = {})
|
64
|
+
options = {
|
65
|
+
'rev' => nil
|
66
|
+
}.update(options)
|
67
|
+
options['rev'] = options['rev'].first if options['rev'].is_a?(Array)
|
68
|
+
|
69
|
+
return respond_with_error(404) unless databases.has_key?(db_name)
|
70
|
+
databases[db_name].delete(doc_id, options['rev'])
|
71
|
+
rescue RockingChair::Error => e
|
72
|
+
e.raise_rest_client_error
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.load(db_name, doc_id, options = {})
|
76
|
+
return respond_with_error(404) unless databases.has_key?(db_name)
|
77
|
+
databases[db_name].load(doc_id, options)
|
78
|
+
rescue RockingChair::Error => e
|
79
|
+
e.raise_rest_client_error
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.load_all(db_name, options = {})
|
83
|
+
return respond_with_error(404) unless databases.has_key?(db_name)
|
84
|
+
databases[db_name].all_documents(options)
|
85
|
+
rescue RockingChair::Error => e
|
86
|
+
e.raise_rest_client_error
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.copy(db_name, doc_id, options = {})
|
90
|
+
return respond_with_error(404) unless databases.has_key?(db_name)
|
91
|
+
destination_id, revision = normalize_url(options['Destination'])
|
92
|
+
databases[db_name].copy(doc_id, destination_id, revision['rev'])
|
93
|
+
rescue RockingChair::Error => e
|
94
|
+
e.raise_rest_client_error
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.bulk(db_name, options = {})
|
98
|
+
return respond_with_error(404) unless databases.has_key?(db_name)
|
99
|
+
databases[db_name].bulk(options)
|
100
|
+
rescue RockingChair::Error => e
|
101
|
+
e.raise_rest_client_error
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.view(db_name, design_doc_id, view_name, options = {})
|
105
|
+
return respond_with_error(404) unless databases.has_key?(db_name)
|
106
|
+
databases[db_name].view(design_doc_id, view_name, options)
|
107
|
+
rescue RockingChair::Error => e
|
108
|
+
e.raise_rest_client_error
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.database(name, parameters)
|
112
|
+
if databases.has_key?(name)
|
113
|
+
respond_with({
|
114
|
+
"db_name" => name,
|
115
|
+
"doc_count" => databases[name].document_count,
|
116
|
+
"doc_del_count" => 0,
|
117
|
+
"update_seq" => 10,
|
118
|
+
"purge_seq" => 0,
|
119
|
+
"compact_running" => false,
|
120
|
+
"disk_size" => 16473,
|
121
|
+
"instance_start_time" => "1265409273572320",
|
122
|
+
"disk_format_version" => 4})
|
123
|
+
else
|
124
|
+
return respond_with_error(404)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.respond_with(obj)
|
129
|
+
obj.to_json
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.respond_with_error(code, message=nil)
|
133
|
+
message ||= 'no such DB'
|
134
|
+
{code => message}.to_json
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|