Some docs.
[teddybear.git] / couch.rb
bloba8e32a1c8e29561d6f0084f74890bb3cfec6a334
1 #! /usr/bin/ruby
2 # Copyright (c) 2007, Stuart Glaser <StuGlaser@gmail.com>
4 # Permission is hereby granted, free of charge, to any person
5 # obtaining a copy of this software and associated documentation
6 # files (the "Software"), to deal in the Software without
7 # restriction, including without limitation the rights to use,
8 # copy, modify, merge, publish, distribute, sublicense, and/or sell
9 # copies of the Software, and to permit persons to whom the
10 # Software is furnished to do so, subject to the following
11 # conditions:
12
13 # The above copyright notice and this permission notice shall be
14 # included in all copies or substantial portions of the Software.
15
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18 # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23 # OTHER DEALINGS IN THE SOFTWARE.
25 require 'net/http'
26 require 'json'
28 # The JSON parser stupidly does not accept just strings.
29 def hacked_json_parse(json)
30   boxed = "[ " + json + " ]"
31   boxed_parsed = JSON.parse boxed
32   boxed_parsed[0]
33 end
35 class CouchException < StandardError
36   attr_reader :couch, :response
38   def initialize(couch, response)
39     @couch = couch
40     @response = response
41   end
43   def couch_backtrace
44     #json = JSON.parse @response.body
45     json = hacked_json_parse @response.body
46     json['error']['id'] + "\n" + json['error']['reason']
47   end
49   def to_s
50     "#{@response.class}\n" +
51       "Couch Backtrace:" + couch_backtrace +
52       "\nRuby Backtrace:"
53   end
54 end
56 module Couch
57   
58   class Server
59     attr_accessor :host, :port
61     def initialize(host, port)
62       @host = host
63       @port = port
64     end
65     
66     def get(path)
67       request Net::HTTP::Get.new(path)
68     end
70     def put(path, data)  # TODO: support text/javascript
71       req = Net::HTTP::Put.new(path)
72       req['content-type'] = 'text/javascript'
73       req['content-type'] = 'application/json'
74       req.body = data
75       request req
76     end
78     def post(path, data)  # TODO: support text/javascript
79       req = Net::HTTP::Post.new(path)
80       req.body = data
81       request req
82     end
84     def delete(path)
85       request Net::HTTP::Delete.new(path)
86     end
88     def request(req)
89       resp = Net::HTTP.start(@host, @port) do |http|
90         http.request req
91       end
92       if not resp.kind_of?(Net::HTTPSuccess)
93         raise CouchException.new(self, resp)
94       end
95       resp
96     end
98   end
101   class Db
102     attr_accessor :server
103     attr_accessor :name
105     def initialize(server, name)
106       @server = server
107       @name = name
108     end
110     def get(doc_id)
111       resp = @server.get doc_path(doc_id)
112       #JSON.parse resp.body
113       hacked_json_parse resp.body
114     end
116     def put(doc_id, body)
117       resp = $server.put doc_path(doc_id), body.to_json
118       #JSON.parse resp.body
119       hacked_json_parse resp.body
120     end
122     def post(body)
123       resp = $server.post doc_path(''), body.to_json
124       #JSON.parse resp.body
125       hacked_json_parse resp.body
126     end
128     def delete(doc_id)
129       resp = $server.delete doc_path(doc_id)
130       #JSON.parse resp.body
131       hacked_json_parse resp.body
132     end
134     def temp_view(data)
135       path = doc_path('_temp_view')
136       req = Net::HTTP::Post.new(path)
137       req['content-type'] = 'text/javascript'
138       req.body = data
139       request req
140     end
141     
142     def temp_view(body)  # TODO: content-type
143       resp = $server.post doc_path('_temp_view'), body
144       #JSON.parse resp.body
145       hacked_json_parse resp.body
146     end
148     def doc_path(doc_id)
149       '/' + @name + '/' + doc_id
150     end
151   end
154   # Represents a document in a Couch database
155   class Doc
156     attr_reader :db
157     attr_accessor :id
158     attr_reader :_rev
159     attr_accessor :_attachments
161     def _attachments
162       @_attachments = [] if @_attachments.nil?
163       @_attachments
164     end
166     class << self
167       attr_reader :field_names
168       
169       def field(*syms)
170         attr_accessor(*syms)
171         @field_names = [] if @field_names.nil?
172         @field_names |= syms
173         nil
174       end
175     end
177     def Doc.find(db, id)
178       begin
179         doc = new $db
180         doc.from_couch! $db.get(id)
181         doc
182       rescue CouchException=>ex
183         return nil if ex.response.instance_of? Net::HTTPNotFound
184         raise
185       end
186     end
187     
189     def Doc.find_or_create(db, id)
190       doc = find db, id
191       return doc unless doc.nil?
193       doc = new db
194       doc.id = id
195       doc.save
196       doc
197     end
199     
200     def initialize(db)
201       @db = db
202     end
204     
205     def save
206       print "SAVE #{id}\n"
207       fields = to_couch
208       results = if @id.nil?  # POST
209                   @db.post fields
210                 else         # PUT
211                   @db.put @id, fields
212                 end
213       @_rev = results['rev']
214       @id = results['id']
215       results
216     end
218     def refresh!
219       raise "Never saved" if @id.nil?
220       self.from_couch! @db.get(@id)
221       nil
222     end
224     def delete!
225       results = @db.delete(@id + "?rev=#{@_rev}")
226       @_rev = nil
227       results
228     end
230     def new_attachment
231       a = Attachment.new self
232       _attachments << a
233       a
234     end
236     # Converts the object to (unparsed) JSON format for couch
237     def to_couch
238       # Fills in the fields for saving to the db
239       fields = {}
240       fields['_rev'] = @_rev unless @_rev.nil?
241       self.class.field_names.each do |field|
242         fields[field] = instance_variable_get '@' + field.to_s
243       end
244       unless _attachments.empty?
245         # Let ye of functional natures rejoice!
246         fields['_attachments'] = _attachments.inject({}) do |all, a|
247           all.merge a.to_couch
248         end
249       end
250       fields
251     end
253     # Takes (parsed) JSON from couch and places it into the fields
254     def from_couch!(couch)
255       @id = couch['_id']
256       @_rev = couch['_rev']
257       self.class.field_names.each do |field|
258         instance_variable_set('@' + field.to_s, couch[field.to_s])
259       end
260       unless couch['_attachments'].nil?
261         @_attachments = []
262         couch['_attachments'].each do |name,value|
263           a = Attachment.new self
264           a.from_couch! name, value
265           @_attachments << a
266         end
267       end
268       nil
269     end
270   end
272   
273   class Attachment
274     attr_accessor :doc
275     attr_accessor :name, :type, :data
276     attr_reader :length, :stub
278     def data=(data)
279       @stub = data.nil?
280       @data = data
281     end
283     def data
284       if not @data.nil?
285         @data
286       else
287         path = @doc.id + '?attachment=' + @name
288         @doc.db.get path
289       end
290     end
292     def initialize(doc)
293       @doc = doc
294       @stub = true
295     end
297     def to_couch
298       a = { 'type' => type, 'stub' => stub }
299       a['data'] = data unless stub
300       { name => a }
301     end
303     def from_couch!(name, couch)
304       @name = name
305       @stub = couch['stub'] or false
306       @type = couch['type']
307       @length = couch['length']
308       @data = nil
309     end
310   end
311   
315 true