careo-makura 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2008 Michael Fellinger <m.fellinger@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Makura
2
+
3
+ Makura is a Ruby wrapper around the CouchDB REST API.
4
+
5
+ It doesn't provide a lot of bells and whistles, but aims to be as close to the
6
+ original API as possible, while taking advantage of Ruby's expressive power.
7
+
8
+ Most ideas for this have been gathered while trying other libraries such as
9
+ CouchObject, CouchRest, and RelaxDB.
10
+
11
+ It does so with almost no modification of ruby libraries and makes it simple
12
+ to switch the HTTP library used by changing one method.
13
+ Eventually Makura will be using an evented http library to provide better
14
+ performance.
15
+
16
+ We are using the json library, which adds following methods:
17
+ To Kernel: #j, #jj, #JSON
18
+ To Object and most core classes: #to_json
19
+ To String: #to_json_raw_object, #to_json, #to_json_raw
20
+
21
+ ## Dependencies
22
+
23
+ * CouchDB - 0.9 trunk (rev 725909 and higher)
24
+ * rest-client
25
+ * rack
26
+ * json
27
+
28
+ ## Features
29
+
30
+ * Simple Models, the CouchDB way and without magic.
31
+ * Free choice of inheritance, just include the Makura::Model module.
32
+ * Smart interpretation of returned JSON.
33
+ * Direct mapping of javascript files to map/reduce functions for views.
34
+ * CouchDB specific error reporting, no meaningless HTTP status code.
35
+ * Live update of views during runtime.
36
+ * Easy configuration, possibility to use different servers and databases each
37
+ model.
38
+
39
+ ## Usage
40
+
41
+ See the /example/blog.rb
data/bin/makura ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'makura'
5
+
6
+ Makura::Model.database = ARGV.shift
7
+ DB = Makura::Model.database
8
+
9
+ puts "Your database is assigned to DB"
10
+
11
+ require 'irb'
12
+ IRB.start
data/example/blog.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'makura'
2
+
3
+ # Setting up everything
4
+
5
+ # Makura::Model.server = 'http://localhost:5984'
6
+ Makura::Model.database = 'mydb'
7
+
8
+ class Post
9
+ include Makura::Model
10
+
11
+ properties :title, :text, :tags
12
+ belongs_to :author
13
+
14
+ layout :all
15
+
16
+ validates(:title){ presence and length :within => (3..100) }
17
+ validates(:text){ presence }
18
+
19
+ save # submit design docs to CouchDB
20
+ end
21
+
22
+ class Author
23
+ include Makura::Model
24
+
25
+ property :name
26
+
27
+ layout :posts, :reduce => :sum_length
28
+ layout :all
29
+
30
+ save
31
+ end
32
+
33
+ class Comment
34
+ include Makura::Model
35
+
36
+ property :text
37
+ end
38
+
39
+ # And here it goes.
40
+
41
+ author = Author.new('name' => 'Michael Fellinger')
42
+ author.save
43
+
44
+ post = Post.new(
45
+ :title => 'Hello, World!',
46
+ :text => 'This is my first post',
47
+ :author => author)
48
+ post.save
49
+
50
+ Post.view(:all).each do |post|
51
+ p post
52
+ p post.author
53
+ end
@@ -0,0 +1,5 @@
1
+ function(doc){
2
+ if(doc.type == 'Author'){
3
+ emit(doc._id, doc);
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ function(doc){
2
+ if(doc.type == 'Post' && doc.user){
3
+ emit(doc.user, doc);
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ function(doc){
2
+ if(doc.type == 'Post'){
3
+ emit(doc._id, doc);
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ function(doc){
2
+ if(doc.type == 'Comment'){
3
+ emit(doc._id, doc);
4
+ }
5
+ }
@@ -0,0 +1,7 @@
1
+ function(keys, values, rereduce){
2
+ if(rereduce){
3
+ return sum(values);
4
+ } else {
5
+ return values.length;
6
+ }
7
+ }
data/lib/makura.rb ADDED
@@ -0,0 +1,62 @@
1
+ require 'pp'
2
+ require 'uri'
3
+
4
+ begin
5
+ require 'rubygems'
6
+ rescue LoadError
7
+ end
8
+
9
+ require 'rest_client'
10
+ require 'json'
11
+
12
+ module Makura
13
+ VERSION = '2008.01.15'
14
+ ROOT = File.expand_path(File.dirname(__FILE__))
15
+ end
16
+
17
+ unless $LOAD_PATH.any?{|lp| File.expand_path(lp) == Makura::ROOT }
18
+ $LOAD_PATH.unshift(Makura::ROOT)
19
+ end
20
+
21
+ require 'makura/error'
22
+ require 'makura/http_methods'
23
+ require 'makura/server'
24
+ require 'makura/database'
25
+ require 'makura/uuid_cache'
26
+ require 'makura/model'
27
+ require 'makura/design'
28
+ require 'makura/layout'
29
+
30
+ module Makura
31
+ CHARS = (48..128).map{|c| c.chr}.grep(/[[:alnum:]]/)
32
+ MOD = CHARS.size
33
+
34
+ module_function
35
+
36
+ # From Rack
37
+ def escape(s)
38
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
39
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
40
+ }.tr(' ', '+')
41
+ end
42
+
43
+ def pretty_from_md5(md5)
44
+ id = md5.to_i(16)
45
+ o = []
46
+ while id > 0
47
+ id, r = id.divmod(MOD)
48
+ o.unshift CHARS[r]
49
+ end
50
+ o.join
51
+ end
52
+
53
+ def pretty_to_md5(id)
54
+ i = 0
55
+ id.scan(/./) do |c|
56
+ i = i * MOD + CHARS.index(c)
57
+ end
58
+ i.to_s(16)
59
+ end
60
+ end
61
+
62
+ Sofa = Makura # be backwards compatible
@@ -0,0 +1,216 @@
1
+ module Makura
2
+ class Database
3
+ include HTTPMethods
4
+ attr_accessor :server, :name
5
+
6
+ # Initialize instance of Database and create if it doesn't exist yet.
7
+ # To prevent automatic creation, pass false as 3rd parameter
8
+ #
9
+ # Usage:
10
+ # server = Makura::Server.new
11
+ # # #<URI::HTTP:0xb7788234 URL:http://localhost:5984>
12
+ # database = Makura::Database.new(server, 'foo')
13
+ # # #<Makura::Database 'http://localhost:5984/foo'>
14
+
15
+ def initialize(server, name, auto_create = true)
16
+ @server, @name = server, name
17
+ create if auto_create
18
+ end
19
+
20
+ # Create the database if it doesn't exist already.
21
+ #
22
+ # Usage:
23
+ # server = Makura::Server.new
24
+ # # #<URI::HTTP:0xb76a4a98 URL:http://localhost:5984>
25
+ #
26
+ # database = Makura::Database.new(server, 'foo', false)
27
+ # # #<Makura::Database 'http://localhost:5984/foo'>
28
+ #
29
+ # database.create
30
+ # # {"update_seq"=>0, "doc_count"=>0, "purge_seq"=>0, "disk_size"=>4096,
31
+ # # "compact_running"=>false, "db_name"=>"foo", "doc_del_count"=>0}
32
+
33
+ def create
34
+ info
35
+ rescue Error::ResourceNotFound
36
+ put("/", :payload => '')
37
+ end
38
+
39
+ # Will delete document in the CouchDB corresponding to given +doc+.
40
+ # Use #destroy to delete the database itself.
41
+ # Use #delete! to automatically rescue exceptions on conflicts.
42
+ #
43
+ # Possible variations (User is a Makura::Model) are:
44
+ #
45
+ # # deleting based on explicit _id and :rev option.
46
+ # database.delete('manveru', :rev => 123134)
47
+ #
48
+ # # deleting based on a Hash
49
+ # database.delete('_id' => 'manveru', '_rev' => 123134)
50
+ #
51
+ # user = User.new(:name => 'manveru')
52
+ # user.save
53
+ # database.delete(user)
54
+ #
55
+ # Usage when deleting document:
56
+ # doc = database.save('name' => 'manveru', 'time' => Time.now)
57
+ # # {"rev"=>"484030692", "id"=>"67e086087d5b7e7196b5c99174b0b66c", "ok"=>true}
58
+ #
59
+ # database[doc['id']]
60
+ # # {"name"=>"manveru", "_rev"=>"484030692",
61
+ # "time"=>"Sat Nov 22 16:37:50 +0900 2008",
62
+ # "_id"=>"67e086087d5b7e7196b5c99174b0b66c"}
63
+ #
64
+ # database.delete(doc['id'], :rev => doc['rev'])
65
+ # # {"rev"=>"2034883605", "id"=>"67e086087d5b7e7196b5c99174b0b66c", "ok"=>true}
66
+ #
67
+ # database[doc['id']]
68
+ # RestClient::ResourceNotFound: RestClient::ResourceNotFound
69
+ #
70
+ # database.delete(doc['id'], :rev => doc['rev'])
71
+ # Makura::RequestFailed: {"reason"=>"Document update conflict.", "error"=>"conflict"}
72
+
73
+ def delete(doc, opts = {})
74
+ case doc
75
+ when Makura::Model
76
+ doc_id, doc_rev = doc._id, doc._rev
77
+ when Hash
78
+ doc_id = doc['_id'] || doc['id'] || doc[:_id] || doc[:id]
79
+ doc_rev = doc['_rev'] || doc['rev'] || doc[:_rev] || doc[:rev]
80
+ else
81
+ doc_id = doc
82
+ end
83
+
84
+ raise(ArgumentError, "document _id wasn't passed") unless doc_id
85
+
86
+ doc_id = Makura.escape(doc_id)
87
+ opts[:rev] ||= doc_rev if doc_rev
88
+
89
+ request(:delete, doc_id.to_s, opts)
90
+ end
91
+
92
+ def delete!(doc, opts = {})
93
+ delete(doc, opts)
94
+ rescue Error::Conflict, Error::ResourceNotFound
95
+ end
96
+
97
+ # Delete the database itself.
98
+ #
99
+ # Usage:
100
+ # database.destroy
101
+ # # {"ok"=>true}
102
+ # database.info
103
+ # # RestClient::ResourceNotFound: RestClient::ResourceNotFound
104
+
105
+ def destroy(opts = {})
106
+ request(:delete, '/', opts)
107
+ end
108
+
109
+ def destroy!(opts = {})
110
+ destroy(opts)
111
+ rescue Error::ResourceNotFound
112
+ end
113
+
114
+ def info
115
+ get('/')
116
+ end
117
+
118
+ def all_docs(params = {})
119
+ get('_all_docs')
120
+ end
121
+ alias documents all_docs
122
+
123
+ def [](id, rev = nil)
124
+ id = Makura.escape(id)
125
+ if rev
126
+ get(id, :rev => rev)
127
+ else
128
+ get(id)
129
+ end
130
+ end
131
+
132
+ def []=(id, doc)
133
+ id = Makura.escape(id)
134
+ put(id, :payload => prepare_doc(doc))
135
+ end
136
+
137
+ def temp_view(params = {})
138
+ params[:payload] = functions = {}
139
+ functions[:map] = params.delete(:map) if params[:map]
140
+ functions[:reduce] = params.delete(:reduce) if params[:reduce]
141
+ params['Content-Type'] = 'application/json'
142
+
143
+ post('_temp_view', params)
144
+ end
145
+
146
+ def view(layout, params = {})
147
+ get("_view/#{layout}", params)
148
+ end
149
+
150
+ def save(doc)
151
+ if id = doc['_id']
152
+ id = Makura.escape(id)
153
+ put(id, :payload => prepare_doc(doc))
154
+ else
155
+ id = doc['_id'] = @server.next_uuid
156
+ id = Makura.escape(id)
157
+ put(id, :payload => prepare_doc(doc))
158
+ end
159
+ end
160
+
161
+ # NOTE:
162
+ # * Seems like we don't even need to check _id, CouchDB will assign it.
163
+ # But in order to use our own uuids we still do it.
164
+ def bulk_docs(docs)
165
+ docs.each{|doc| doc['_id'] ||= @server.next_uuid }
166
+ post("_bulk_docs", :payload => {:docs => docs})
167
+ end
168
+ alias bulk_save bulk_docs
169
+
170
+ def get_attachment(doc, file_id)
171
+ doc_id = doc.respond_to?(:_id) ? doc._id : doc.to_str
172
+ file_id, doc_id = Makura.escape(file_id), Makura.escape(doc_id)
173
+
174
+ get("#{doc_id}/#{file_id}", :raw => true)
175
+ end
176
+
177
+ # PUT an attachment directly to CouchDB
178
+ def put_attachment(doc, file_id, file, options = {})
179
+ doc_id, file_id = Makura.escape(doc._id), Makura.escape(file_id)
180
+
181
+ options[:payload] = file
182
+ options[:raw] = true
183
+ options[:rev] = doc._rev if doc._rev
184
+
185
+ put("#{doc_id}/#{file_id}", options)
186
+ end
187
+
188
+ def prepare_doc(doc)
189
+ if attachments = doc['_attachments']
190
+ doc['_attachments'] = encode_attachments(attachments)
191
+ end
192
+
193
+ return doc
194
+ end
195
+
196
+ def request(method, path, params = {})
197
+ @server.send(:request, method, "/#{name}/#{path}", params)
198
+ end
199
+
200
+ def encode_attachments(attachments)
201
+ attachments.each do |key, value|
202
+ next if value['stub']
203
+ value['data'] = base64(value['data'])
204
+ end
205
+ attachments
206
+ end
207
+
208
+ def base64(data)
209
+ [data.to_s].pack('m').delete("\n")
210
+ end
211
+
212
+ def inspect
213
+ "#<Makura::Database '#{@server.uri(name)}'>"
214
+ end
215
+ end
216
+ end