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