makura 2009.05.27

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/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,50 @@
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
+ save # submit design docs to CouchDB
17
+ end
18
+
19
+ class Author
20
+ include Makura::Model
21
+
22
+ property :name
23
+
24
+ layout :posts, :reduce => :sum_length
25
+ layout :all
26
+
27
+ save
28
+ end
29
+
30
+ class Comment
31
+ include Makura::Model
32
+
33
+ property :text
34
+ end
35
+
36
+ # And here it goes.
37
+
38
+ author = Author.new('name' => 'Michael Fellinger')
39
+ author.save
40
+
41
+ post = Post.new(
42
+ :title => 'Hello, World!',
43
+ :text => 'This is my first post',
44
+ :author => author)
45
+ post.save
46
+
47
+ Post.view(:all).each do |post|
48
+ p post
49
+ p post.author
50
+ 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,66 @@
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
+ ROOT = File.expand_path(File.dirname(__FILE__))
14
+ end
15
+
16
+ unless $LOAD_PATH.any?{|lp| File.expand_path(lp) == Makura::ROOT }
17
+ $LOAD_PATH.unshift(Makura::ROOT)
18
+ end
19
+
20
+ require 'makura/version'
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
+
61
+ def constant(name, root = Module)
62
+ name.split('::').inject(root){|s,v| s.const_get(v) }
63
+ end
64
+ end
65
+
66
+ 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("_design/#{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
@@ -0,0 +1,37 @@
1
+ module Makura
2
+ class Design
3
+ attr_accessor :database, :name, :language
4
+ attr_reader :layouts
5
+
6
+ def initialize(name, database = nil)
7
+ @name, @database = name, database
8
+ @language = 'javascript'
9
+ @layouts = {}
10
+ end
11
+
12
+ def save
13
+ hash = to_hash
14
+ doc = @database[hash['_id']]
15
+ doc['views'] = hash['views']
16
+ @database.save(doc)
17
+ rescue Makura::Error::ResourceNotFound
18
+ @database.save(to_hash)
19
+ end
20
+
21
+ def [](layout_name)
22
+ @layouts[layout_name.to_s]
23
+ end
24
+
25
+ def []=(layout_name, layout)
26
+ @layouts[layout_name.to_s] = layout
27
+ end
28
+
29
+ def to_hash
30
+ views = {}
31
+ @layouts.each{|name, layout| views[name] = layout.to_hash }
32
+ views.delete_if{|k,v| !(v[:map] || v['map']) }
33
+
34
+ {'language' => @language, '_id' => "_design/#{@name}", 'views' => views}
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ module Makura
2
+ # Mother and namespace of all exceptions
3
+ class Error < ::RuntimeError
4
+ class ConnectionRefused < Error; end
5
+ class RequestFailed < Error; end
6
+ class ResourceNotFound < RequestFailed; end
7
+ class Conflict < RequestFailed; end
8
+ class MissingRevision < RequestFailed; end
9
+ class BadRequest < RequestFailed; end
10
+ class Authorization < RequestFailed; end
11
+ class NotFound < RequestFailed; end
12
+ class FileExists < RequestFailed; end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module Makura
2
+ module HTTPMethods
3
+ def delete(path, params = {})
4
+ request(:delete, path, params)
5
+ end
6
+
7
+ def get(path, params = {})
8
+ request(:get, path, params)
9
+ end
10
+
11
+ def post(path, params = {})
12
+ request(:post, path, params)
13
+ end
14
+
15
+ def put(path, params = {})
16
+ request(:put, path, params)
17
+ end
18
+ end
19
+ end