chill 1
Sign up to get free protection for your applications and to get access to all the features.
- data/library/chill.rb +379 -0
- metadata +79 -0
data/library/chill.rb
ADDED
@@ -0,0 +1,379 @@
|
|
1
|
+
# Bluebie's silly little CouchDB abstraction
|
2
|
+
require 'json'
|
3
|
+
require 'uuid'
|
4
|
+
require 'rest-client'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module ChillDB
|
8
|
+
def self.goes database_name, *args
|
9
|
+
submod = Module.new do
|
10
|
+
extend ChillDB
|
11
|
+
@@database = ChillDB::Database.new database_name, *args
|
12
|
+
@@templates = {}
|
13
|
+
end
|
14
|
+
self.constants(false).each do |const|
|
15
|
+
submod.const_set(const, self.const_get(const));
|
16
|
+
end
|
17
|
+
Object.const_set(database_name, submod)
|
18
|
+
end
|
19
|
+
|
20
|
+
# stores a list of templates which can be used to make a new document
|
21
|
+
def templates obj
|
22
|
+
@@templates.merge!(obj) if obj and obj.is_a? Hash
|
23
|
+
return @@templates
|
24
|
+
end
|
25
|
+
|
26
|
+
# get a new document consisting of a template
|
27
|
+
def template kind
|
28
|
+
properties = @@templates[kind].dup
|
29
|
+
properties[:kind] = kind.to_s
|
30
|
+
ChillDB::Document.new(@@database, properties)
|
31
|
+
end
|
32
|
+
|
33
|
+
# get a design with a particular name - or make one!
|
34
|
+
def design name
|
35
|
+
ChillDB::Design.new @@database, name
|
36
|
+
end
|
37
|
+
|
38
|
+
# get or make a document with a particular id/name, or just a blank new one
|
39
|
+
def document id = false
|
40
|
+
if id
|
41
|
+
ChillDB::Document.load(@@database, id)
|
42
|
+
else
|
43
|
+
ChillDB::Document.new(@@database)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
alias_method :[], :document
|
47
|
+
|
48
|
+
def []= document, hash
|
49
|
+
raise "Not a hash?" unless hash.is_a? Hash
|
50
|
+
hash = hash.dup
|
51
|
+
hash['_id'] = document
|
52
|
+
return hash.commit! if hash.is_a? ChillDB::Document
|
53
|
+
return ChillDB::Document.new(@@database, hash).commit!
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class ChillDB::Database
|
58
|
+
attr_reader :url, :meta
|
59
|
+
def initialize name, settings = {}
|
60
|
+
@meta = {} # little place to store our things
|
61
|
+
@url = URI::HTTP.build(
|
62
|
+
host: settings[:host] || 'localhost',
|
63
|
+
port: settings[:port] || 5984,
|
64
|
+
userinfo: [settings[:user], settings[:pass]],
|
65
|
+
path: "/#{URI.escape hyphenate(name)}/"
|
66
|
+
)
|
67
|
+
|
68
|
+
# make this database if it doesn't exist yet
|
69
|
+
$stderr.puts "New database created at #{@url}" if http('').put('').code == 201
|
70
|
+
end
|
71
|
+
|
72
|
+
# ask the server to compact this database
|
73
|
+
def compact!
|
74
|
+
request = http('_compact').post('')
|
75
|
+
raise request.body unless request.code == 202
|
76
|
+
return self
|
77
|
+
end
|
78
|
+
|
79
|
+
#def revs_limit; http('_revs_limit').get.body.to_i; end
|
80
|
+
#def revs_limit=(v); http('_revs_limit').put(v.to_s); end
|
81
|
+
|
82
|
+
# grab a RestClient http resource for this database
|
83
|
+
def http resource
|
84
|
+
RestClient::Resource.new((@url + resource).to_s, :headers => {accept: 'application/json', content_type: 'application/json'}) { |r| r }
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
# a little utility to hyphenate a string
|
90
|
+
def hyphenate string
|
91
|
+
string.to_s.gsub(/(.)([A-Z])/, '\1-\2').downcase
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
# handles conversion of symbol keys to strings, and method accessors
|
104
|
+
class ChillDB::IndifferentHash < Hash
|
105
|
+
def initialize *args
|
106
|
+
super(*args) do |hash, key| # indifferent access
|
107
|
+
hash[key.to_s] if Symbol === key
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# getters and setters for hash items
|
112
|
+
def method_missing name, *args
|
113
|
+
return self[name.to_s] if self[name.to_s]
|
114
|
+
return self[name.to_s[0...-1]] = args.first if name.to_s.end_with? '=' and args.length == 1
|
115
|
+
super
|
116
|
+
end
|
117
|
+
|
118
|
+
# make hash thing indifferent
|
119
|
+
[:merge, :merge!, :replace, :update, :update!].each do |name|
|
120
|
+
define_method name do |*args,&proc|
|
121
|
+
super(normalize_hash(args.shift), *args, &proc)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# make hash thing indifferent
|
126
|
+
[:has_key?, :include?, :key?, :member?, :delete].each do |name|
|
127
|
+
define_method name do |first, *seconds,&proc|
|
128
|
+
first = first.to_s if first.is_a? Symbol
|
129
|
+
super(first, *seconds, &proc)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def []= key, value
|
134
|
+
key = key.to_s if key.is_a? Symbol
|
135
|
+
super(key, normalize(value))
|
136
|
+
end
|
137
|
+
|
138
|
+
# return an actual hash
|
139
|
+
def to_hash
|
140
|
+
Hash.new.replace self
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
private
|
145
|
+
# normalises all symbols in a hash in to string keys
|
146
|
+
def normalize_hash original_hash
|
147
|
+
hash = {}
|
148
|
+
original_hash.each do |key,value|
|
149
|
+
key = key.to_s if key.is_a? Symbol
|
150
|
+
hash[key] = normalize value
|
151
|
+
end
|
152
|
+
return hash
|
153
|
+
end
|
154
|
+
|
155
|
+
def normalize thing
|
156
|
+
return ChillDB::IndifferentHash.new.replace(thing) if thing.is_a? Hash
|
157
|
+
return thing.map { |i| normalize(i) } if thing.is_a? Array
|
158
|
+
return thing;
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
|
164
|
+
|
165
|
+
|
166
|
+
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
class ChillDB::Document < ChillDB::IndifferentHash
|
171
|
+
attr_reader :database
|
172
|
+
|
173
|
+
def initialize database, values = false
|
174
|
+
@database = database
|
175
|
+
super()
|
176
|
+
|
177
|
+
if values.is_a? Symbol
|
178
|
+
reset @database.class_variable_get(:@@templates)[values]
|
179
|
+
elsif values
|
180
|
+
reset values
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def reset values
|
185
|
+
self.replace values
|
186
|
+
self['_id'] ||= UUID.new.generate # generate an _id if we don't have one already
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.load database, docid
|
190
|
+
new(database, _id: docid).load
|
191
|
+
end
|
192
|
+
|
193
|
+
# load this document from server in to this local cache
|
194
|
+
# returns self, or if there's a more specific subclass, a subclassed version of Document
|
195
|
+
def load lookup_revision = nil
|
196
|
+
url = URI(URI.escape self['_id'])
|
197
|
+
url.query = "rev=#{lookup_revision}" if lookup_revision
|
198
|
+
response = @database.http(url).get
|
199
|
+
return self if response.code == 404 # we can create it later
|
200
|
+
raise response.body if response.code != 200
|
201
|
+
|
202
|
+
reset JSON.parse(response.body)
|
203
|
+
return self
|
204
|
+
end
|
205
|
+
|
206
|
+
# stores updates (or newly existingness) of document to origin database
|
207
|
+
def commit!
|
208
|
+
json = JSON.generate(self)
|
209
|
+
response = @database.http(URI.escape self['_id']).put(json);
|
210
|
+
raise response.body unless (200..299).include? response.code
|
211
|
+
json = JSON.parse(response.body)
|
212
|
+
raise "Not ok! #{response.body}" unless json['ok']
|
213
|
+
self['_id'] = json['id']
|
214
|
+
self['_rev'] = json['rev']
|
215
|
+
return self
|
216
|
+
end
|
217
|
+
|
218
|
+
# delete this jerk
|
219
|
+
def delete!
|
220
|
+
response = @database.http(URI.escape self['_id']).delete()
|
221
|
+
response = ChillDB::IndifferentHash.new.replace JSON.parse(response.body)
|
222
|
+
raise "Couldn't delete #{self._id}: #{response.error} - #{response.reason}" if response['error']
|
223
|
+
return response
|
224
|
+
end
|
225
|
+
|
226
|
+
# set current revision to a different thing
|
227
|
+
def revision= new_revision
|
228
|
+
load new_revision
|
229
|
+
end
|
230
|
+
|
231
|
+
# gets a list of revisions
|
232
|
+
def revisions
|
233
|
+
request = @database.http("#{URI.escape self['_id']}?revs=true").get
|
234
|
+
json = JSON.parse(request.body)
|
235
|
+
json['_revisions']['ids']
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
|
241
|
+
|
242
|
+
|
243
|
+
|
244
|
+
class ChillDB::List < Array
|
245
|
+
attr_accessor :total_rows, :offset, :database
|
246
|
+
|
247
|
+
# store rows nicely in mah belleh
|
248
|
+
def rows=(arr)
|
249
|
+
self.replace arr.map { |item| ChillDB::IndifferentHash.new.replace(item) }
|
250
|
+
end
|
251
|
+
|
252
|
+
# we are the rows!
|
253
|
+
def rows
|
254
|
+
self
|
255
|
+
end
|
256
|
+
|
257
|
+
def ids
|
258
|
+
self.map { |i| i['id'] }
|
259
|
+
end
|
260
|
+
|
261
|
+
def keys
|
262
|
+
self.map { |i| i['key'] }
|
263
|
+
end
|
264
|
+
|
265
|
+
def values
|
266
|
+
self.map { |i| i['value'] }
|
267
|
+
end
|
268
|
+
|
269
|
+
def each_pair &proc
|
270
|
+
self.each { |item| proc.call(item['key'], item['value']) }
|
271
|
+
end
|
272
|
+
|
273
|
+
# make a regular ruby hash version
|
274
|
+
def to_h
|
275
|
+
hash = ChillDB::IndifferentHash.new
|
276
|
+
|
277
|
+
each do |item|
|
278
|
+
hash[item['key']] = item['value']
|
279
|
+
end
|
280
|
+
|
281
|
+
return hash
|
282
|
+
end
|
283
|
+
alias_method :to_hash, :to_h
|
284
|
+
|
285
|
+
# remove all the documents in this list from the database
|
286
|
+
def delete_all!
|
287
|
+
each { |item|
|
288
|
+
raise "Not all documents listed are emitted as values in this view" if item.id != item.value._id
|
289
|
+
}
|
290
|
+
|
291
|
+
request = { docs: map { |item|
|
292
|
+
{ _id: item.value._id, _rev: item.value._rev, _deleted: true }
|
293
|
+
} }
|
294
|
+
|
295
|
+
response = JSON.parse @database.http("_bulk_docs").post(request.to_json)
|
296
|
+
raise "Error: #{response['error']} - #{response['reason']}" if response.is_a? Hash and response['error']
|
297
|
+
|
298
|
+
return ChillDB::IndifferentHash.new.replace response
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
|
303
|
+
|
304
|
+
|
305
|
+
|
306
|
+
|
307
|
+
|
308
|
+
class ChillDB::Design
|
309
|
+
attr_accessor :name # can rename with that - though leaves old copy on server probably
|
310
|
+
|
311
|
+
def initialize database, name
|
312
|
+
@database = database
|
313
|
+
@name = name.to_s
|
314
|
+
end
|
315
|
+
|
316
|
+
# lazy load document - referencing this causes load from database
|
317
|
+
def document
|
318
|
+
@document ||= ChillDB::Document::load @database, "_design/#{@name}"
|
319
|
+
@document['language'] ||= 'javascript'
|
320
|
+
@document['views'] ||= {}
|
321
|
+
return @document
|
322
|
+
end
|
323
|
+
|
324
|
+
# adds views
|
325
|
+
def add_views collection
|
326
|
+
document['views'].merge! collection
|
327
|
+
return self
|
328
|
+
end
|
329
|
+
|
330
|
+
# sets views
|
331
|
+
def views collection
|
332
|
+
document['views'] = {}
|
333
|
+
add_views collection
|
334
|
+
return self
|
335
|
+
end
|
336
|
+
|
337
|
+
# store changes to server
|
338
|
+
def commit!
|
339
|
+
document['_id'] = "_design/#{@name}"
|
340
|
+
document.commit!
|
341
|
+
end
|
342
|
+
|
343
|
+
# query a view - response is an array augmented with methods like arr.total_rows
|
344
|
+
def query view, options = {}
|
345
|
+
if options[:range]
|
346
|
+
range = options.delete[:range]
|
347
|
+
options[:startkey], options[:endkey] = range.first, range.last
|
348
|
+
options[:startkey], options[:endkey] = options[:endkey], options[:startkey] if options[:descending]
|
349
|
+
options[:inclusive_end] = !range.exclude_end?
|
350
|
+
end
|
351
|
+
|
352
|
+
# these options need to be json encoded
|
353
|
+
[:key, :startkey, :endkey, :keys, :descending, :limit, :skip, :group, :group_level,
|
354
|
+
:reduce, :include_docs, :inclusive_end, :update_seq
|
355
|
+
].each do |name|
|
356
|
+
options[name] = options[name].to_json if options.has_key? name
|
357
|
+
end
|
358
|
+
|
359
|
+
opts = options.map { |key, value| "#{URI.escape(key.to_s)}=#{URI.escape(value.to_s)}" }.join('&')
|
360
|
+
url = "_design/#{URI.escape @name}/_view/#{URI.escape view.to_s}?#{opts}"
|
361
|
+
response = @database.http(url).get()
|
362
|
+
json = JSON.parse response.body
|
363
|
+
raise "#{json['error']} - #{json['reason']} @ #{url}" if json['error']
|
364
|
+
|
365
|
+
# put the results in to a QueryResults object - a glorified array
|
366
|
+
results = ChillDB::List.new
|
367
|
+
results.database = @database
|
368
|
+
json.each do |key, value|
|
369
|
+
results.send("#{key}=", value)
|
370
|
+
end
|
371
|
+
|
372
|
+
return results
|
373
|
+
end
|
374
|
+
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
|
379
|
+
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: chill
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bluebie
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-01 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: json
|
16
|
+
requirement: &70155679871420 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.6.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70155679871420
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rest-client
|
27
|
+
requirement: &70155679870940 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.6.7
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70155679870940
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: uuid
|
38
|
+
requirement: &70155679870480 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.3.4
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70155679870480
|
47
|
+
description: A little library to talk to a couchdb. I made it skinny, because couchdb
|
48
|
+
is very simple. I think that's a good thing.
|
49
|
+
email: a@creativepony.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- library/chill.rb
|
55
|
+
homepage: http://github.com/Bluebie/chill
|
56
|
+
licenses: []
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
require_paths:
|
60
|
+
- .
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.8.5
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: A tiny plug to hook ruby in to couchdb
|
79
|
+
test_files: []
|