juozasg-couchrest 0.10.1

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.
Files changed (55) hide show
  1. data/LICENSE +176 -0
  2. data/README.rdoc +67 -0
  3. data/Rakefile +86 -0
  4. data/THANKS +15 -0
  5. data/bin/couchapp +58 -0
  6. data/bin/couchdir +20 -0
  7. data/bin/couchview +48 -0
  8. data/examples/model/example.rb +138 -0
  9. data/examples/word_count/markov +38 -0
  10. data/examples/word_count/views/books/chunked-map.js +3 -0
  11. data/examples/word_count/views/books/united-map.js +1 -0
  12. data/examples/word_count/views/markov/chain-map.js +6 -0
  13. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  14. data/examples/word_count/views/word_count/count-map.js +6 -0
  15. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  16. data/examples/word_count/word_count.rb +67 -0
  17. data/examples/word_count/word_count_query.rb +39 -0
  18. data/lib/couchrest/commands/generate.rb +71 -0
  19. data/lib/couchrest/commands/push.rb +103 -0
  20. data/lib/couchrest/core/database.rb +173 -0
  21. data/lib/couchrest/core/design.rb +89 -0
  22. data/lib/couchrest/core/document.rb +60 -0
  23. data/lib/couchrest/core/model.rb +557 -0
  24. data/lib/couchrest/core/server.rb +51 -0
  25. data/lib/couchrest/core/view.rb +4 -0
  26. data/lib/couchrest/helper/file_manager.rb +317 -0
  27. data/lib/couchrest/helper/pager.rb +103 -0
  28. data/lib/couchrest/helper/streamer.rb +44 -0
  29. data/lib/couchrest/helper/templates/bar.txt +11 -0
  30. data/lib/couchrest/helper/templates/example-map.js +8 -0
  31. data/lib/couchrest/helper/templates/example-reduce.js +10 -0
  32. data/lib/couchrest/helper/templates/index.html +26 -0
  33. data/lib/couchrest/monkeypatches.rb +24 -0
  34. data/lib/couchrest.rb +125 -0
  35. data/spec/couchapp_spec.rb +87 -0
  36. data/spec/couchrest/core/couchrest_spec.rb +191 -0
  37. data/spec/couchrest/core/database_spec.rb +478 -0
  38. data/spec/couchrest/core/design_spec.rb +131 -0
  39. data/spec/couchrest/core/document_spec.rb +96 -0
  40. data/spec/couchrest/core/model_spec.rb +660 -0
  41. data/spec/couchrest/helpers/file_manager_spec.rb +203 -0
  42. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  43. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  44. data/spec/fixtures/attachments/couchdb.png +0 -0
  45. data/spec/fixtures/attachments/test.html +11 -0
  46. data/spec/fixtures/views/lib.js +3 -0
  47. data/spec/fixtures/views/test_view/lib.js +3 -0
  48. data/spec/fixtures/views/test_view/only-map.js +4 -0
  49. data/spec/fixtures/views/test_view/test-map.js +3 -0
  50. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  51. data/spec/spec.opts +6 -0
  52. data/spec/spec_helper.rb +14 -0
  53. data/utils/remap.rb +27 -0
  54. data/utils/subset.rb +30 -0
  55. metadata +154 -0
@@ -0,0 +1,3 @@
1
+ function(key,values){
2
+ return sum(values);
3
+ }
@@ -0,0 +1,67 @@
1
+ require File.dirname(__FILE__) + '/../../couchrest'
2
+
3
+ couch = CouchRest.new("http://localhost:5984")
4
+ db = couch.database('word-count-example')
5
+ db.delete! rescue nil
6
+ db = couch.create_db('word-count-example')
7
+
8
+ books = {
9
+ 'outline-of-science.txt' => 'http://www.gutenberg.org/files/20417/20417.txt',
10
+ 'ulysses.txt' => 'http://www.gutenberg.org/dirs/etext03/ulyss12.txt',
11
+ 'america.txt' => 'http://www.gutenberg.org/files/16960/16960.txt',
12
+ 'da-vinci.txt' => 'http://www.gutenberg.org/dirs/etext04/7ldv110.txt'
13
+ }
14
+
15
+ books.each do |file, url|
16
+ pathfile = File.join(File.dirname(__FILE__),file)
17
+ `curl #{url} > #{pathfile}` unless File.exists?(pathfile)
18
+ end
19
+
20
+
21
+ books.keys.each do |book|
22
+ title = book.split('.')[0]
23
+ puts title
24
+ File.open(File.join(File.dirname(__FILE__),book),'r') do |file|
25
+ lines = []
26
+ chunk = 0
27
+ while line = file.gets
28
+ lines << line
29
+ if lines.length > 10
30
+ db.save({
31
+ :title => title,
32
+ :chunk => chunk,
33
+ :text => lines.join('')
34
+ })
35
+ chunk += 1
36
+ puts chunk
37
+ lines = []
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ # word_count = {
44
+ # :map => 'function(doc){
45
+ # var words = doc.text.split(/\W/);
46
+ # words.forEach(function(word){
47
+ # if (word.length > 0) emit([word,doc.title],1);
48
+ # });
49
+ # }',
50
+ # :reduce => 'function(key,combine){
51
+ # return sum(combine);
52
+ # }'
53
+ # }
54
+ #
55
+ # db.delete db.get("_design/word_count") rescue nil
56
+ #
57
+ # db.save({
58
+ # "_id" => "_design/word_count",
59
+ # :views => {
60
+ # :count => word_count,
61
+ # :words => {:map => word_count[:map]}
62
+ # }
63
+ # })
64
+
65
+ # puts "The books have been stored in your CouchDB. To initiate the MapReduce process, visit http://localhost:5984/_utils/ in your browser and click 'word-count-example', then select view 'words' or 'count'. The process could take about 15 minutes on an average MacBook."
66
+ #
67
+
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + '/../../couchrest'
2
+
3
+ couch = CouchRest.new("http://localhost:5984")
4
+ db = couch.database('word-count-example')
5
+
6
+ puts "Now that we've parsed all those books into CouchDB, the queries we can run are incredibly flexible."
7
+ puts "\nThe simplest query we can run is the total word count for all words in all documents:"
8
+
9
+ puts db.view('word_count/count').inspect
10
+
11
+ puts "\nWe can also narrow the query down to just one word, across all documents. Here is the count for 'flight' in all three books:"
12
+
13
+ word = 'flight'
14
+ params = {
15
+ :startkey => [word],
16
+ :endkey => [word,'Z']
17
+ }
18
+
19
+ puts db.view('word_count/count',params).inspect
20
+
21
+ puts "\nWe scope the query using startkey and endkey params to take advantage of CouchDB's collation ordering. Here are the params for the last query:"
22
+ puts params.inspect
23
+
24
+ puts "\nWe can also count words on a per-title basis."
25
+
26
+ title = 'da-vinci'
27
+ params = {
28
+ :key => [word, title]
29
+ }
30
+
31
+ puts db.view('word_count/count',params).inspect
32
+
33
+
34
+ puts "\nHere are the params for 'flight' in the da-vinci book:"
35
+ puts params.inspect
36
+ puts
37
+ puts 'The url looks like this:'
38
+ puts 'http://localhost:5984/word-count-example/_view/word_count/count?key=["flight","da-vinci"]'
39
+ puts "\nTry dropping that in your browser..."
@@ -0,0 +1,71 @@
1
+ require 'fileutils'
2
+
3
+ module CouchRest
4
+ module Commands
5
+ module Generate
6
+
7
+ def self.run(options)
8
+ directory = options[:directory]
9
+ design_names = options[:trailing_args]
10
+
11
+ FileUtils.mkdir_p(directory)
12
+ filename = File.join(directory, "lib.js")
13
+ self.write(filename, <<-FUNC)
14
+ // Put global functions here.
15
+ // Include in your views with
16
+ //
17
+ // //include-lib
18
+ FUNC
19
+
20
+ design_names.each do |design_name|
21
+ subdirectory = File.join(directory, design_name)
22
+ FileUtils.mkdir_p(subdirectory)
23
+ filename = File.join(subdirectory, "sample-map.js")
24
+ self.write(filename, <<-FUNC)
25
+ function(doc) {
26
+ // Keys is first letter of _id
27
+ emit(doc._id[0], doc);
28
+ }
29
+ FUNC
30
+
31
+ filename = File.join(subdirectory, "sample-reduce.js")
32
+ self.write(filename, <<-FUNC)
33
+ function(keys, values) {
34
+ // Count the number of keys starting with this letter
35
+ return values.length;
36
+ }
37
+ FUNC
38
+
39
+ filename = File.join(subdirectory, "lib.js")
40
+ self.write(filename, <<-FUNC)
41
+ // Put functions specific to '#{design_name}' here.
42
+ // Include in your views with
43
+ //
44
+ // //include-lib
45
+ FUNC
46
+ end
47
+ end
48
+
49
+ def self.help
50
+ helpstring = <<-GEN
51
+
52
+ Usage: couchview generate directory design1 design2 design3 ...
53
+
54
+ Couchview will create directories and example views for the design documents you specify.
55
+
56
+ GEN
57
+ helpstring.gsub(/^ /, '')
58
+ end
59
+
60
+ def self.write(filename, contents)
61
+ puts "Writing #{filename}"
62
+ File.open(filename, "w") do |f|
63
+ # Remove leading spaces
64
+ contents.gsub!(/^ ( )?/, '')
65
+ f.write contents
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,103 @@
1
+ module CouchRest
2
+
3
+ module Commands
4
+
5
+ module Push
6
+
7
+ def self.run(options)
8
+ directory = options[:directory]
9
+ database = options[:trailing_args].first
10
+
11
+ fm = CouchRest::FileManager.new(database)
12
+ fm.loud = options[:loud]
13
+
14
+ if options[:loud]
15
+ puts "Pushing views from directory #{directory} to database #{fm.db}"
16
+ end
17
+
18
+ fm.push_views(directory)
19
+ end
20
+
21
+ def self.help
22
+ helpstring = <<-GEN
23
+
24
+ == Pushing views with Couchview ==
25
+
26
+ Usage: couchview push directory dbname
27
+
28
+ Couchview expects a specific filesystem layout for your CouchDB views (see
29
+ example below). It also supports advanced features like inlining of library
30
+ code (so you can keep DRY) as well as avoiding unnecessary document
31
+ modification.
32
+
33
+ Couchview also solves a problem with CouchDB's view API, which only provides
34
+ access to the final reduce side of any views which have both a map and a
35
+ reduce function defined. The intermediate map results are often useful for
36
+ development and production. CouchDB is smart enough to reuse map indexes for
37
+ functions duplicated across views within the same design document.
38
+
39
+ For views with a reduce function defined, Couchview creates both a reduce view
40
+ and a map-only view, so that you can browse and query the map side as well as
41
+ the reduction, with no performance penalty.
42
+
43
+ == Example ==
44
+
45
+ couchview push foo-project/bar-views baz-database
46
+
47
+ This will push the views defined in foo-project/bar-views into a database
48
+ called baz-database. Couchview expects the views to be defined in files with
49
+ names like:
50
+
51
+ foo-project/bar-views/my-design/viewname-map.js
52
+ foo-project/bar-views/my-design/viewname-reduce.js
53
+ foo-project/bar-views/my-design/noreduce-map.js
54
+
55
+ Pushed to => http://localhost:5984/baz-database/_design/my-design
56
+
57
+ And the design document:
58
+ {
59
+ "views" : {
60
+ "viewname-map" : {
61
+ "map" : "### contents of view-name-map.js ###"
62
+ },
63
+ "viewname-reduce" : {
64
+ "map" : "### contents of view-name-map.js ###",
65
+ "reduce" : "### contents of view-name-reduce.js ###"
66
+ },
67
+ "noreduce-map" : {
68
+ "map" : "### contents of noreduce-map.js ###"
69
+ }
70
+ }
71
+ }
72
+
73
+ Couchview will create a design document for each subdirectory of the views
74
+ directory specified on the command line.
75
+
76
+ == Library Inlining ==
77
+
78
+ Couchview can optionally inline library code into your views so you only have
79
+ to maintain it in one place. It looks for any files named lib.* in your
80
+ design-doc directory (for doc specific libs) and in the parent views directory
81
+ (for project global libs). These libraries are only inserted into views which
82
+ include the text
83
+
84
+ //include-lib
85
+
86
+ or
87
+
88
+ #include-lib
89
+
90
+ Couchview is a result of scratching my own itch. I'd be happy to make it more
91
+ general, so please contact me at jchris@grabb.it if you'd like to see anything
92
+ added or changed.
93
+
94
+ GEN
95
+ helpstring.gsub(/^ /, '')
96
+ end
97
+
98
+ end
99
+
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,173 @@
1
+ require 'cgi'
2
+ require "base64"
3
+
4
+ module CouchRest
5
+ class Database
6
+ attr_reader :server, :host, :name, :root
7
+
8
+ # Create a CouchRest::Database adapter for the supplied CouchRest::Server
9
+ # and database name.
10
+ #
11
+ # ==== Parameters
12
+ # server<CouchRest::Server>:: database host
13
+ # name<String>:: database name
14
+ #
15
+ def initialize server, name
16
+ @name = name
17
+ @server = server
18
+ @host = server.uri
19
+ @root = "#{host}/#{name}"
20
+ @streamer = Streamer.new(self)
21
+ end
22
+
23
+ # returns the database's uri
24
+ def to_s
25
+ @root
26
+ end
27
+
28
+ # GET the database info from CouchDB
29
+ def info
30
+ CouchRest.get @root
31
+ end
32
+
33
+ # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
34
+ def documents params = {}
35
+ keys = params.delete(:keys)
36
+ url = CouchRest.paramify_url "#{@root}/_all_docs", params
37
+ if keys
38
+ CouchRest.post(url, {:keys => keys})
39
+ else
40
+ CouchRest.get url
41
+ end
42
+ end
43
+
44
+ # POST a temporary view function to CouchDB for querying. This is not
45
+ # recommended, as you don't get any performance benefit from CouchDB's
46
+ # materialized views. Can be quite slow on large databases.
47
+ def temp_view funcs, params = {}
48
+ keys = params.delete(:keys)
49
+ funcs = funcs.merge({:keys => keys}) if keys
50
+ url = CouchRest.paramify_url "#{@root}/_temp_view", params
51
+ JSON.parse(RestClient.post(url, funcs.to_json, {"Content-Type" => 'application/json'}))
52
+ end
53
+
54
+ # Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
55
+ # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
56
+ def view name, params = {}, &block
57
+ keys = params.delete(:keys)
58
+ url = CouchRest.paramify_url "#{@root}/_view/#{name}", params
59
+ if keys
60
+ CouchRest.post(url, {:keys => keys})
61
+ else
62
+ if block_given?
63
+ @streamer.view(name, params, &block)
64
+ else
65
+ CouchRest.get url
66
+ end
67
+ end
68
+ end
69
+
70
+ # GET a document from CouchDB, by id. Returns a Ruby Hash.
71
+ def get id
72
+ slug = CGI.escape(id)
73
+ hash = CouchRest.get("#{@root}/#{slug}")
74
+ doc = if /^_design/ =~ hash["_id"]
75
+ Design.new(hash)
76
+ else
77
+ Document.new(hash)
78
+ end
79
+ doc.database = self
80
+ doc
81
+ end
82
+
83
+ # GET an attachment directly from CouchDB
84
+ def fetch_attachment doc, name
85
+ doc = CGI.escape(doc)
86
+ name = CGI.escape(name)
87
+ RestClient.get "#{@root}/#{doc}/#{name}"
88
+ end
89
+
90
+ # PUT an attachment directly to CouchDB
91
+ def put_attachment doc, name, file, options = {}
92
+ docid = CGI.escape(doc['_id'])
93
+ name = CGI.escape(name)
94
+ uri = if doc['_rev']
95
+ "#{@root}/#{docid}/#{name}?rev=#{doc['_rev']}"
96
+ else
97
+ "#{@root}/#{docid}/#{name}"
98
+ end
99
+
100
+ JSON.parse(RestClient.put(uri, file, options))
101
+ end
102
+
103
+ # Save a document to CouchDB. This will use the <tt>_id</tt> field from
104
+ # the document as the id for PUT, or request a new UUID from CouchDB, if
105
+ # no <tt>_id</tt> is present on the document. IDs are attached to
106
+ # documents on the client side because POST has the curious property of
107
+ # being automatically retried by proxies in the event of network
108
+ # segmentation and lost responses.
109
+ def save doc
110
+ if doc['_attachments']
111
+ doc['_attachments'] = encode_attachments(doc['_attachments'])
112
+ end
113
+ result = if doc['_id']
114
+ slug = CGI.escape(doc['_id'])
115
+ CouchRest.put "#{@root}/#{slug}", doc
116
+ else
117
+ begin
118
+ slug = doc['_id'] = @server.next_uuid
119
+ CouchRest.put "#{@root}/#{slug}", doc
120
+ rescue #old version of couchdb
121
+ CouchRest.post @root, doc
122
+ end
123
+ end
124
+ if result['ok']
125
+ doc['_id'] = result['id']
126
+ doc['_rev'] = result['rev']
127
+ doc.database = self if doc.respond_to?(:database=)
128
+ end
129
+ result
130
+ end
131
+
132
+ # POST an array of documents to CouchDB. If any of the documents are
133
+ # missing ids, supply one from the uuid cache.
134
+ def bulk_save docs
135
+ ids, noids = docs.partition{|d|d['_id']}
136
+ uuid_count = [noids.length, @server.uuid_batch_count].max
137
+ noids.each do |doc|
138
+ nextid = @server.next_uuid(uuid_count) rescue nil
139
+ doc['_id'] = nextid if nextid
140
+ end
141
+ CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
142
+ end
143
+
144
+ # DELETE the document from CouchDB that has the given <tt>_id</tt> and
145
+ # <tt>_rev</tt>.
146
+ def delete doc
147
+ raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev']
148
+
149
+ slug = CGI.escape(doc['_id'])
150
+ CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
151
+ end
152
+
153
+ # DELETE the database itself. This is not undoable and could be rather
154
+ # catastrophic. Use with care!
155
+ def delete!
156
+ CouchRest.delete @root
157
+ end
158
+
159
+ private
160
+
161
+ def encode_attachments attachments
162
+ attachments.each do |k,v|
163
+ next if v['stub']
164
+ v['data'] = base64(v['data'])
165
+ end
166
+ attachments
167
+ end
168
+
169
+ def base64 data
170
+ Base64.encode64(data).gsub(/\s/,'')
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,89 @@
1
+ module CouchRest
2
+ class Design < Document
3
+ def view_by *keys
4
+ opts = keys.pop if keys.last.is_a?(Hash)
5
+ opts ||= {}
6
+ self['views'] ||= {}
7
+ method_name = "by_#{keys.join('_and_')}"
8
+
9
+ if opts[:map]
10
+ view = {}
11
+ view['map'] = opts.delete(:map)
12
+ if opts[:reduce]
13
+ view['reduce'] = opts.delete(:reduce)
14
+ opts[:reduce] = false
15
+ end
16
+ self['views'][method_name] = view
17
+ else
18
+ doc_keys = keys.collect{|k|"doc['#{k}']"} # this is where :require => 'doc.x == true' would show up
19
+ key_emit = doc_keys.length == 1 ? "#{doc_keys.first}" : "[#{doc_keys.join(', ')}]"
20
+ guards = opts.delete(:guards) || []
21
+ guards.concat doc_keys
22
+ map_function = <<-JAVASCRIPT
23
+ function(doc) {
24
+ if (#{guards.join(' && ')}) {
25
+ emit(#{key_emit}, null);
26
+ }
27
+ }
28
+ JAVASCRIPT
29
+ self['views'][method_name] = {
30
+ 'map' => map_function
31
+ }
32
+ end
33
+ self['views'][method_name]['couchrest-defaults'] = opts unless opts.empty?
34
+ method_name
35
+ end
36
+
37
+ # Dispatches to any named view.
38
+ def view view_name, query={}, &block
39
+ view_name = view_name.to_s
40
+ view_slug = "#{name}/#{view_name}"
41
+ defaults = (self['views'][view_name] && self['views'][view_name]["couchrest-defaults"]) || {}
42
+ fetch_view(view_slug, defaults.merge(query), &block)
43
+ end
44
+
45
+ def name
46
+ id.sub('_design/','') if id
47
+ end
48
+
49
+ def name= newname
50
+ self['_id'] = "_design/#{newname}"
51
+ end
52
+
53
+ def save
54
+ raise ArgumentError, "_design docs require a name" unless name && name.length > 0
55
+ super
56
+ end
57
+
58
+ private
59
+
60
+ # returns stored defaults if the there is a view named this in the design doc
61
+ def has_view?(view)
62
+ view = view.to_s
63
+ self['views'][view] &&
64
+ (self['views'][view]["couchrest-defaults"]||{})
65
+ end
66
+
67
+ # def fetch_view_with_docs name, opts, raw=false, &block
68
+ # if raw
69
+ # fetch_view name, opts, &block
70
+ # else
71
+ # begin
72
+ # view = fetch_view name, opts.merge({:include_docs => true}), &block
73
+ # view['rows'].collect{|r|new(r['doc'])} if view['rows']
74
+ # rescue
75
+ # # fallback for old versions of couchdb that don't
76
+ # # have include_docs support
77
+ # view = fetch_view name, opts, &block
78
+ # view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
79
+ # end
80
+ # end
81
+ # end
82
+
83
+ def fetch_view view_name, opts, &block
84
+ database.view(view_name, opts, &block)
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -0,0 +1,60 @@
1
+ module CouchRest
2
+ class Response < Hash
3
+ def initialize keys = {}
4
+ keys.each do |k,v|
5
+ self[k.to_s] = v
6
+ end
7
+ end
8
+ def []= key, value
9
+ super(key.to_s, value)
10
+ end
11
+ def [] key
12
+ super(key.to_s)
13
+ end
14
+ end
15
+
16
+ class Document < Response
17
+
18
+ attr_accessor :database
19
+
20
+ # alias for self['_id']
21
+ def id
22
+ self['_id']
23
+ end
24
+
25
+ # alias for self['_rev']
26
+ def rev
27
+ self['_rev']
28
+ end
29
+
30
+ # returns true if the document has never been saved
31
+ def new_document?
32
+ !rev
33
+ end
34
+
35
+ # Saves the document to the db using create or update. Also runs the :save
36
+ # callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on
37
+ # CouchDB's response.
38
+ def save
39
+ raise ArgumentError, "doc.database required for saving" unless database
40
+ result = database.save self
41
+ result['ok']
42
+ end
43
+
44
+ # Deletes the document from the database. Runs the :delete callbacks.
45
+ # Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
46
+ # document to be saved to a new <tt>_id</tt>.
47
+ def destroy
48
+ raise ArgumentError, "doc.database required to destroy" unless database
49
+ result = database.delete self
50
+ if result['ok']
51
+ self['_rev'] = nil
52
+ self['_id'] = nil
53
+ end
54
+ result['ok']
55
+ end
56
+
57
+ end
58
+
59
+
60
+ end