rocking_chair 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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