samlown-couchrest 0.35

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 (105) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +46 -0
  3. data/Rakefile +67 -0
  4. data/THANKS.md +19 -0
  5. data/examples/model/example.rb +144 -0
  6. data/examples/word_count/markov +38 -0
  7. data/examples/word_count/views/books/chunked-map.js +3 -0
  8. data/examples/word_count/views/books/united-map.js +1 -0
  9. data/examples/word_count/views/markov/chain-map.js +6 -0
  10. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  11. data/examples/word_count/views/word_count/count-map.js +6 -0
  12. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  13. data/examples/word_count/word_count.rb +46 -0
  14. data/examples/word_count/word_count_query.rb +40 -0
  15. data/examples/word_count/word_count_views.rb +26 -0
  16. data/history.txt +114 -0
  17. data/lib/couchrest/commands/generate.rb +71 -0
  18. data/lib/couchrest/commands/push.rb +103 -0
  19. data/lib/couchrest/core/adapters/restclient.rb +35 -0
  20. data/lib/couchrest/core/database.rb +377 -0
  21. data/lib/couchrest/core/design.rb +79 -0
  22. data/lib/couchrest/core/document.rb +84 -0
  23. data/lib/couchrest/core/http_abstraction.rb +48 -0
  24. data/lib/couchrest/core/response.rb +16 -0
  25. data/lib/couchrest/core/rest_api.rb +49 -0
  26. data/lib/couchrest/core/server.rb +88 -0
  27. data/lib/couchrest/core/view.rb +4 -0
  28. data/lib/couchrest/helper/pager.rb +103 -0
  29. data/lib/couchrest/helper/streamer.rb +51 -0
  30. data/lib/couchrest/helper/upgrade.rb +51 -0
  31. data/lib/couchrest/middlewares/logger.rb +263 -0
  32. data/lib/couchrest/mixins/attachments.rb +31 -0
  33. data/lib/couchrest/mixins/attribute_protection.rb +74 -0
  34. data/lib/couchrest/mixins/callbacks.rb +532 -0
  35. data/lib/couchrest/mixins/class_proxy.rb +124 -0
  36. data/lib/couchrest/mixins/collection.rb +260 -0
  37. data/lib/couchrest/mixins/design_doc.rb +103 -0
  38. data/lib/couchrest/mixins/document_queries.rb +80 -0
  39. data/lib/couchrest/mixins/extended_attachments.rb +70 -0
  40. data/lib/couchrest/mixins/extended_document_mixins.rb +9 -0
  41. data/lib/couchrest/mixins/properties.rb +154 -0
  42. data/lib/couchrest/mixins/validation.rb +246 -0
  43. data/lib/couchrest/mixins/views.rb +173 -0
  44. data/lib/couchrest/mixins.rb +4 -0
  45. data/lib/couchrest/monkeypatches.rb +113 -0
  46. data/lib/couchrest/more/casted_model.rb +58 -0
  47. data/lib/couchrest/more/extended_document.rb +310 -0
  48. data/lib/couchrest/more/property.rb +50 -0
  49. data/lib/couchrest/more/typecast.rb +175 -0
  50. data/lib/couchrest/support/blank.rb +42 -0
  51. data/lib/couchrest/support/class.rb +190 -0
  52. data/lib/couchrest/support/rails.rb +42 -0
  53. data/lib/couchrest/validation/auto_validate.rb +157 -0
  54. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  55. data/lib/couchrest/validation/validation_errors.rb +125 -0
  56. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  57. data/lib/couchrest/validation/validators/confirmation_validator.rb +107 -0
  58. data/lib/couchrest/validation/validators/format_validator.rb +122 -0
  59. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  60. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  61. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  62. data/lib/couchrest/validation/validators/length_validator.rb +139 -0
  63. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  64. data/lib/couchrest/validation/validators/numeric_validator.rb +109 -0
  65. data/lib/couchrest/validation/validators/required_field_validator.rb +114 -0
  66. data/lib/couchrest.rb +162 -0
  67. data/spec/couchrest/core/couchrest_spec.rb +184 -0
  68. data/spec/couchrest/core/database_spec.rb +840 -0
  69. data/spec/couchrest/core/design_spec.rb +138 -0
  70. data/spec/couchrest/core/document_spec.rb +275 -0
  71. data/spec/couchrest/core/server_spec.rb +35 -0
  72. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  73. data/spec/couchrest/helpers/streamer_spec.rb +52 -0
  74. data/spec/couchrest/more/attribute_protection_spec.rb +150 -0
  75. data/spec/couchrest/more/casted_extended_doc_spec.rb +79 -0
  76. data/spec/couchrest/more/casted_model_spec.rb +406 -0
  77. data/spec/couchrest/more/extended_doc_attachment_spec.rb +135 -0
  78. data/spec/couchrest/more/extended_doc_inherited_spec.rb +40 -0
  79. data/spec/couchrest/more/extended_doc_spec.rb +797 -0
  80. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  81. data/spec/couchrest/more/extended_doc_view_spec.rb +456 -0
  82. data/spec/couchrest/more/property_spec.rb +628 -0
  83. data/spec/fixtures/attachments/README +3 -0
  84. data/spec/fixtures/attachments/couchdb.png +0 -0
  85. data/spec/fixtures/attachments/test.html +11 -0
  86. data/spec/fixtures/more/article.rb +35 -0
  87. data/spec/fixtures/more/card.rb +22 -0
  88. data/spec/fixtures/more/cat.rb +20 -0
  89. data/spec/fixtures/more/course.rb +22 -0
  90. data/spec/fixtures/more/event.rb +8 -0
  91. data/spec/fixtures/more/invoice.rb +17 -0
  92. data/spec/fixtures/more/person.rb +9 -0
  93. data/spec/fixtures/more/question.rb +6 -0
  94. data/spec/fixtures/more/service.rb +12 -0
  95. data/spec/fixtures/more/user.rb +22 -0
  96. data/spec/fixtures/views/lib.js +3 -0
  97. data/spec/fixtures/views/test_view/lib.js +3 -0
  98. data/spec/fixtures/views/test_view/only-map.js +4 -0
  99. data/spec/fixtures/views/test_view/test-map.js +3 -0
  100. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  101. data/spec/spec.opts +6 -0
  102. data/spec/spec_helper.rb +49 -0
  103. data/utils/remap.rb +27 -0
  104. data/utils/subset.rb +30 -0
  105. metadata +223 -0
data/history.txt ADDED
@@ -0,0 +1,114 @@
1
+ == Next Version
2
+
3
+ * Major enhancements
4
+ * Adds support for continuous replication (sauy7)
5
+ * Automatic Type Casting (Alexander Uvarov, Sam Lown, Tim Heighes, Will Leinweber)
6
+ * Added a search method to CouchRest:Database to search the documents in a
7
+ given database. (Dave Farkas, Arnaud Berthomier, John Wood)
8
+ * Minor enhancements
9
+ * Provide a description of the timeout error (John Wood)
10
+
11
+ == 0.35
12
+
13
+ * Major enhancements
14
+ * CouchRest::ExtendedDocument allow chaining the inherit class callback (Kenneth Kalmer) - http://github.com/couchrest/couchrest/issues#issue/8
15
+
16
+ * Minor enhancements
17
+ * Fix attachment bug (Johannes Jörg Schmidt)
18
+ * Fix create database exception bug (Damien Mathieu)
19
+ * Compatible with restclient >= 1.4.0 new responses (Julien Kirch)
20
+ * Bug fix: Attribute protection no longer strips attributes coming from the database (Will Leinweber)
21
+ * Bug fix: Remove double CGI escape when PUTting an attachment (nzoschke)
22
+ * Bug fix: Changing Class proxy to set database on result sets (Peter Gumeson)
23
+ * Bug fix: Updated time regexp (Nolan Darilek)
24
+ * Added an update_doc method to database to handle conflicts during atomic updates. (Pierre Larochelle)
25
+ * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton)
26
+
27
+ == 0.34
28
+
29
+ * Major enhancements
30
+
31
+ * Added support for https database URIs. (Mathias Meyer)
32
+ * Changing some validations to be compatible with activemodel. (Marcos Tapajós)
33
+ * Adds attribute protection to properties. (Will Leinweber)
34
+ * Improved CouchRest::Database#save_doc, added "batch" mode to significantly speed up saves at cost of lower durability gurantees. (Igal Koshevoy)
35
+ * Added CouchRest::Database#bulk_save_doc and #batch_save_doc as human-friendlier wrappers around #save_doc. (Igal Koshevoy)
36
+
37
+ * Minor enhancements
38
+
39
+ * Fix content_type handling for attachments
40
+ * Fixed a bug in the pagination code that caused it to paginate over records outside of the scope of the view parameters.(John Wood)
41
+ * Removed amount_pages calculation for the pagination collection, since it cannot be reliably calculated without a view.(John Wood)
42
+ * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton)
43
+ * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/1 (Marcos Tapajós)
44
+ * Removed the Database class deprecation notices (Matt Aimonetti)
45
+ * Adding support to :cast_as => 'Date'. (Marcos Tapajós)
46
+ * Improve documentation (Marcos Tapajós)
47
+ * Streamer fixes (Julien Sanchez)
48
+ * Fix Save on Document & ExtendedDocument crashed if bulk (Julien Sanchez)
49
+ * Fix Initialization of ExtendentDocument model shouldn't failed on a nil value in argument (deepj)
50
+ * Change to use Jeweler and Gemcutter (Marcos Tapajós)
51
+
52
+ == 0.33
53
+
54
+ * Major enhancements
55
+
56
+ * Added a new Rack logger middleware letting you log/save requests/queries (Matt Aimonetti)
57
+
58
+ * Minor enhancements
59
+
60
+ * Added #amount_pages to a paginated result array (Matt Aimonetti)
61
+ * Ruby 1.9.2 compatible (Matt Aimonetti)
62
+ * Added a property? method for property cast as :boolean (John Wood)
63
+ * Added an option to force the deletion of a attachments (bypass 409s) (Matt Aimonetti)
64
+ * Created a new abstraction layer for the REST API (Matt Aimonetti)
65
+ * Bug fix: made ExtendedDocument#all compatible with Couch 0.10 (tc)
66
+
67
+ == 0.32
68
+
69
+ * Major enhancements
70
+
71
+ * ExtendedDocument.get doesn't raise an exception anymore. If no documents are found nil is returned.
72
+ * ExtendedDocument.get! works the say #get used to work and will raise an exception if a document isn't found.
73
+
74
+ * Minor enhancements
75
+
76
+ * Bug fix: Model.all(:keys => [1,2]) was not working (Matt Aimonetti)
77
+ * Added ValidationErrors#count in order to play nicely with Rails (Peter Wagenet)
78
+ * Bug fix: class proxy design doc refresh (Daniel Kirsh)
79
+ * Bug fix: the count method on the proxy collection was missing (Daniel Kirsch)
80
+ * Added #amount_pages to a paginated collection. (Matt Aimonetti)
81
+
82
+ == 0.31
83
+
84
+ * Major enhancements
85
+
86
+ * Created an abstraction HTTP layer to support different http adapters (Matt Aimonetti)
87
+ * Added ExtendedDocument.create({}) and #create!({}) so you don't have to do Model.new.create (Matt Aimonetti)
88
+
89
+ * Minor enhancements
90
+
91
+ * Added an init.rb file for easy usage as a Rails plugin (Aaron Quint)
92
+ * Bug fix: pagination shouldn't die on empty results (Arnaud Berthomier)
93
+ * Optimized ExtendedDocument.count to run about 3x faster (Matt Aimonetti)
94
+ * Added Float casting (Ryan Felton & Matt Aimonetti)
95
+
96
+ == 0.30
97
+
98
+ * Major enhancements
99
+
100
+ * Added support for pagination (John Wood)
101
+ * Improved performance when initializing documents with timestamps (Matt Aimonetti)
102
+
103
+ * Minor enhancements
104
+
105
+ * Extended the API to retrieve an attachment URI (Matt Aimonetti)
106
+ * Bug fix: default value should be able to be set as false (Alexander Uvarov)
107
+ * Bug fix: validates_is_numeric should be able to properly validate a Float instance (Rob Kaufman)
108
+ * Bug fix: fixed the Timeout implementation (Seth Falcon)
109
+
110
+
111
+ ---
112
+
113
+ Unfortunately, before 0.30 we did not keep a track of the modifications made to CouchRest.
114
+ You can see the full commit history on GitHub: http://github.com/couchrest/couchrest/commits/master/
@@ -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://127.0.0.1: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,35 @@
1
+ module RestClientAdapter
2
+
3
+ module API
4
+ def proxy=(url)
5
+ RestClient.proxy = url
6
+ end
7
+
8
+ def proxy
9
+ RestClient.proxy
10
+ end
11
+
12
+ def get(uri, headers={})
13
+ RestClient.get(uri, headers).to_s
14
+ end
15
+
16
+ def post(uri, payload, headers={})
17
+ RestClient.post(uri, payload, headers).to_s
18
+ end
19
+
20
+ def put(uri, payload, headers={})
21
+ RestClient.put(uri, payload, headers).to_s
22
+ end
23
+
24
+ def delete(uri, headers={})
25
+ RestClient.delete(uri, headers).to_s
26
+ end
27
+
28
+ def copy(uri, headers)
29
+ RestClient::Request.execute( :method => :copy,
30
+ :url => uri,
31
+ :headers => headers).to_s
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,377 @@
1
+ require 'cgi'
2
+ require "base64"
3
+
4
+ module CouchRest
5
+ class Database
6
+ attr_reader :server, :host, :name, :root, :uri
7
+ attr_accessor :bulk_save_cache_limit
8
+
9
+ # Create a CouchRest::Database adapter for the supplied CouchRest::Server
10
+ # and database name.
11
+ #
12
+ # ==== Parameters
13
+ # server<CouchRest::Server>:: database host
14
+ # name<String>:: database name
15
+ #
16
+ def initialize(server, name)
17
+ @name = name
18
+ @server = server
19
+ @host = server.uri
20
+ @uri = "/#{name.gsub('/','%2F')}"
21
+ @root = host + uri
22
+ @streamer = Streamer.new(self)
23
+ @bulk_save_cache = []
24
+ @bulk_save_cache_limit = 500 # must be smaller than the uuid count
25
+ end
26
+
27
+ # returns the database's uri
28
+ def to_s
29
+ @root
30
+ end
31
+
32
+ # GET the database info from CouchDB
33
+ def info
34
+ CouchRest.get @root
35
+ end
36
+
37
+ # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
38
+ def documents(params = {})
39
+ keys = params.delete(:keys)
40
+ url = CouchRest.paramify_url "#{@root}/_all_docs", params
41
+ if keys
42
+ CouchRest.post(url, {:keys => keys})
43
+ else
44
+ CouchRest.get url
45
+ end
46
+ end
47
+
48
+ # Query a CouchDB-Lucene search view
49
+ def search(name, params={})
50
+ # -> http://localhost:5984/yourdb/_fti/YourDesign/by_name?include_docs=true&q=plop*'
51
+ url = CouchRest.paramify_url "#{root}/_fti/#{name}", params
52
+ CouchRest.get url
53
+ end
54
+
55
+ # load a set of documents by passing an array of ids
56
+ def get_bulk(ids)
57
+ documents(:keys => ids, :include_docs => true)
58
+ end
59
+ alias :bulk_load :get_bulk
60
+
61
+ # POST a temporary view function to CouchDB for querying. This is not
62
+ # recommended, as you don't get any performance benefit from CouchDB's
63
+ # materialized views. Can be quite slow on large databases.
64
+ def slow_view(funcs, params = {})
65
+ keys = params.delete(:keys)
66
+ funcs = funcs.merge({:keys => keys}) if keys
67
+ url = CouchRest.paramify_url "#{@root}/_temp_view", params
68
+ JSON.parse(HttpAbstraction.post(url, funcs.to_json, {"Content-Type" => 'application/json'}))
69
+ end
70
+
71
+ # backwards compatibility is a plus
72
+ alias :temp_view :slow_view
73
+
74
+ # Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
75
+ # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
76
+ def view(name, params = {}, &block)
77
+ keys = params.delete(:keys)
78
+ name = name.split('/') # I think this will always be length == 2, but maybe not...
79
+ dname = name.shift
80
+ vname = name.join('/')
81
+ url = CouchRest.paramify_url "#{@root}/_design/#{dname}/_view/#{vname}", params
82
+ if keys
83
+ CouchRest.post(url, {:keys => keys})
84
+ else
85
+ if block_given?
86
+ @streamer.view("_design/#{dname}/_view/#{vname}", params, &block)
87
+ else
88
+ CouchRest.get url
89
+ end
90
+ end
91
+ end
92
+
93
+ # GET a document from CouchDB, by id. Returns a Ruby Hash.
94
+ def get(id, params = {})
95
+ slug = escape_docid(id)
96
+ url = CouchRest.paramify_url("#{@root}/#{slug}", params)
97
+ result = CouchRest.get(url)
98
+ return result unless result.is_a?(Hash)
99
+ doc = if /^_design/ =~ result["_id"]
100
+ Design.new(result)
101
+ else
102
+ Document.new(result)
103
+ end
104
+ doc.database = self
105
+ doc
106
+ end
107
+
108
+ # GET an attachment directly from CouchDB
109
+ def fetch_attachment(doc, name)
110
+ uri = url_for_attachment(doc, name)
111
+ HttpAbstraction.get uri
112
+ end
113
+
114
+ # PUT an attachment directly to CouchDB
115
+ def put_attachment(doc, name, file, options = {})
116
+ docid = escape_docid(doc['_id'])
117
+ uri = url_for_attachment(doc, name)
118
+ JSON.parse(HttpAbstraction.put(uri, file, options))
119
+ end
120
+
121
+ # DELETE an attachment directly from CouchDB
122
+ def delete_attachment(doc, name, force=false)
123
+ uri = url_for_attachment(doc, name)
124
+ # this needs a rev
125
+ begin
126
+ JSON.parse(HttpAbstraction.delete(uri))
127
+ rescue Exception => error
128
+ if force
129
+ # get over a 409
130
+ doc = get(doc['_id'])
131
+ uri = url_for_attachment(doc, name)
132
+ JSON.parse(HttpAbstraction.delete(uri))
133
+ else
134
+ error
135
+ end
136
+ end
137
+ end
138
+
139
+ # Save a document to CouchDB. This will use the <tt>_id</tt> field from
140
+ # the document as the id for PUT, or request a new UUID from CouchDB, if
141
+ # no <tt>_id</tt> is present on the document. IDs are attached to
142
+ # documents on the client side because POST has the curious property of
143
+ # being automatically retried by proxies in the event of network
144
+ # segmentation and lost responses.
145
+ #
146
+ # If <tt>bulk</tt> is true (false by default) the document is cached for bulk-saving later.
147
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
148
+ #
149
+ # If <tt>batch</tt> is true (false by default) the document is saved in
150
+ # batch mode, "used to achieve higher throughput at the cost of lower
151
+ # guarantees. When [...] sent using this option, it is not immediately
152
+ # written to disk. Instead it is stored in memory on a per-user basis for a
153
+ # second or so (or the number of docs in memory reaches a certain point).
154
+ # After the threshold has passed, the docs are committed to disk. Instead
155
+ # of waiting for the doc to be written to disk before responding, CouchDB
156
+ # sends an HTTP 202 Accepted response immediately. batch=ok is not suitable
157
+ # for crucial data, but it ideal for applications like logging which can
158
+ # accept the risk that a small proportion of updates could be lost due to a
159
+ # crash."
160
+ def save_doc(doc, bulk = false, batch = false)
161
+ if doc['_attachments']
162
+ doc['_attachments'] = encode_attachments(doc['_attachments'])
163
+ end
164
+ if bulk
165
+ @bulk_save_cache << doc
166
+ bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
167
+ return {"ok" => true} # Compatibility with Document#save
168
+ elsif !bulk && @bulk_save_cache.length > 0
169
+ bulk_save
170
+ end
171
+ result = if doc['_id']
172
+ slug = escape_docid(doc['_id'])
173
+ begin
174
+ uri = "#{@root}/#{slug}"
175
+ uri << "?batch=ok" if batch
176
+ CouchRest.put uri, doc
177
+ rescue HttpAbstraction::ResourceNotFound
178
+ p "resource not found when saving even tho an id was passed"
179
+ slug = doc['_id'] = @server.next_uuid
180
+ CouchRest.put "#{@root}/#{slug}", doc
181
+ end
182
+ else
183
+ begin
184
+ slug = doc['_id'] = @server.next_uuid
185
+ CouchRest.put "#{@root}/#{slug}", doc
186
+ rescue #old version of couchdb
187
+ CouchRest.post @root, doc
188
+ end
189
+ end
190
+ if result['ok']
191
+ doc['_id'] = result['id']
192
+ doc['_rev'] = result['rev']
193
+ doc.database = self if doc.respond_to?(:database=)
194
+ end
195
+ result
196
+ end
197
+
198
+ # Save a document to CouchDB in bulk mode. See #save_doc's +bulk+ argument.
199
+ def bulk_save_doc(doc)
200
+ save_doc(doc, true)
201
+ end
202
+
203
+ # Save a document to CouchDB in batch mode. See #save_doc's +batch+ argument.
204
+ def batch_save_doc(doc)
205
+ save_doc(doc, false, true)
206
+ end
207
+
208
+ # POST an array of documents to CouchDB. If any of the documents are
209
+ # missing ids, supply one from the uuid cache.
210
+ #
211
+ # If called with no arguments, bulk saves the cache of documents to be bulk saved.
212
+ def bulk_save(docs = nil, use_uuids = true)
213
+ if docs.nil?
214
+ docs = @bulk_save_cache
215
+ @bulk_save_cache = []
216
+ end
217
+ if (use_uuids)
218
+ ids, noids = docs.partition{|d|d['_id']}
219
+ uuid_count = [noids.length, @server.uuid_batch_count].max
220
+ noids.each do |doc|
221
+ nextid = @server.next_uuid(uuid_count) rescue nil
222
+ doc['_id'] = nextid if nextid
223
+ end
224
+ end
225
+ CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
226
+ end
227
+ alias :bulk_delete :bulk_save
228
+
229
+ # DELETE the document from CouchDB that has the given <tt>_id</tt> and
230
+ # <tt>_rev</tt>.
231
+ #
232
+ # If <tt>bulk</tt> is true (false by default) the deletion is recorded for bulk-saving (bulk-deletion :) later.
233
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
234
+ def delete_doc(doc, bulk = false)
235
+ raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev']
236
+ if bulk
237
+ @bulk_save_cache << { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true }
238
+ return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
239
+ return { "ok" => true } # Mimic the non-deferred version
240
+ end
241
+ slug = escape_docid(doc['_id'])
242
+ CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
243
+ end
244
+
245
+ # COPY an existing document to a new id. If the destination id currently exists, a rev must be provided.
246
+ # <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
247
+ # hash with a '_rev' key
248
+ def copy_doc(doc, dest)
249
+ raise ArgumentError, "_id is required for copying" unless doc['_id']
250
+ slug = escape_docid(doc['_id'])
251
+ destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev']
252
+ "#{dest['_id']}?rev=#{dest['_rev']}"
253
+ else
254
+ dest
255
+ end
256
+ CouchRest.copy "#{@root}/#{slug}", destination
257
+ end
258
+
259
+ # Updates the given doc by yielding the current state of the doc
260
+ # and trying to update update_limit times. Returns the new doc
261
+ # if the doc was successfully updated without hitting the limit
262
+ def update_doc(doc_id, params = {}, update_limit=10)
263
+ resp = {'ok' => false}
264
+ new_doc = nil
265
+ last_fail = nil
266
+
267
+ until resp['ok'] or update_limit <= 0
268
+ doc = self.get(doc_id, params) # grab the doc
269
+ new_doc = yield doc # give it to the caller to be updated
270
+ begin
271
+ resp = self.save_doc new_doc # try to PUT the updated doc into the db
272
+ rescue RestClient::RequestFailed => e
273
+ if e.http_code == 409 # Update collision
274
+ update_limit -= 1
275
+ last_fail = e
276
+ else # some other error
277
+ raise e
278
+ end
279
+ end
280
+ end
281
+
282
+ raise last_fail unless resp['ok']
283
+ new_doc
284
+ end
285
+
286
+ # Compact the database, removing old document revisions and optimizing space use.
287
+ def compact!
288
+ CouchRest.post "#{@root}/_compact"
289
+ end
290
+
291
+ # Create the database
292
+ def create!
293
+ bool = server.create_db(@name) rescue false
294
+ bool && true
295
+ end
296
+
297
+ # Delete and re create the database
298
+ def recreate!
299
+ delete!
300
+ create!
301
+ rescue RestClient::ResourceNotFound
302
+ ensure
303
+ create!
304
+ end
305
+
306
+ # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts.
307
+ def replicate_from other_db, continuous=false
308
+ replicate other_db, continuous, :target => name
309
+ end
310
+
311
+ # Replicates via "pushing" to another database. Makes no attempt to deal with conflicts.
312
+ def replicate_to other_db, continuous=false
313
+ replicate other_db, continuous, :source => name
314
+ end
315
+
316
+ # DELETE the database itself. This is not undoable and could be rather
317
+ # catastrophic. Use with care!
318
+ def delete!
319
+ clear_extended_doc_fresh_cache
320
+ CouchRest.delete @root
321
+ end
322
+
323
+ private
324
+
325
+ def replicate other_db, continuous, options
326
+ raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
327
+ raise ArgumentError, "must provide a target or source option" unless (options.key?(:target) || options.key?(:source))
328
+ payload = options
329
+ if options.has_key?(:target)
330
+ payload[:source] = other_db.root
331
+ else
332
+ payload[:target] = other_db.root
333
+ end
334
+ payload[:continuous] = continuous
335
+ CouchRest.post "#{@host}/_replicate", payload
336
+ end
337
+
338
+ def clear_extended_doc_fresh_cache
339
+ ::CouchRest::ExtendedDocument.subclasses.each{|klass| klass.design_doc_fresh = false if klass.respond_to?(:design_doc_fresh=) }
340
+ end
341
+
342
+ def uri_for_attachment(doc, name)
343
+ if doc.is_a?(String)
344
+ puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id"
345
+ docid = doc
346
+ rev = nil
347
+ else
348
+ docid = doc['_id']
349
+ rev = doc['_rev']
350
+ end
351
+ docid = escape_docid(docid)
352
+ name = CGI.escape(name)
353
+ rev = "?rev=#{doc['_rev']}" if rev
354
+ "/#{docid}/#{name}#{rev}"
355
+ end
356
+
357
+ def url_for_attachment(doc, name)
358
+ @root + uri_for_attachment(doc, name)
359
+ end
360
+
361
+ def escape_docid id
362
+ /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
363
+ end
364
+
365
+ def encode_attachments(attachments)
366
+ attachments.each do |k,v|
367
+ next if v['stub']
368
+ v['data'] = base64(v['data'])
369
+ end
370
+ attachments
371
+ end
372
+
373
+ def base64(data)
374
+ Base64.encode64(data).gsub(/\s/,'')
375
+ end
376
+ end
377
+ end