gbuesing-couchrest 0.23

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 (92) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +93 -0
  3. data/Rakefile +75 -0
  4. data/THANKS.md +18 -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/lib/couchrest.rb +198 -0
  17. data/lib/couchrest/commands/generate.rb +71 -0
  18. data/lib/couchrest/commands/push.rb +103 -0
  19. data/lib/couchrest/core/database.rb +298 -0
  20. data/lib/couchrest/core/design.rb +79 -0
  21. data/lib/couchrest/core/document.rb +87 -0
  22. data/lib/couchrest/core/response.rb +16 -0
  23. data/lib/couchrest/core/server.rb +103 -0
  24. data/lib/couchrest/core/view.rb +4 -0
  25. data/lib/couchrest/helper/pager.rb +103 -0
  26. data/lib/couchrest/helper/streamer.rb +44 -0
  27. data/lib/couchrest/helper/upgrade.rb +51 -0
  28. data/lib/couchrest/mixins.rb +4 -0
  29. data/lib/couchrest/mixins/attachments.rb +31 -0
  30. data/lib/couchrest/mixins/callbacks.rb +483 -0
  31. data/lib/couchrest/mixins/class_proxy.rb +108 -0
  32. data/lib/couchrest/mixins/design_doc.rb +85 -0
  33. data/lib/couchrest/mixins/document_queries.rb +48 -0
  34. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  35. data/lib/couchrest/mixins/extended_document_mixins.rb +7 -0
  36. data/lib/couchrest/mixins/properties.rb +129 -0
  37. data/lib/couchrest/mixins/validation.rb +242 -0
  38. data/lib/couchrest/mixins/views.rb +175 -0
  39. data/lib/couchrest/monkeypatches.rb +113 -0
  40. data/lib/couchrest/more/casted_model.rb +28 -0
  41. data/lib/couchrest/more/extended_document.rb +215 -0
  42. data/lib/couchrest/more/property.rb +40 -0
  43. data/lib/couchrest/support/blank.rb +42 -0
  44. data/lib/couchrest/support/class.rb +176 -0
  45. data/lib/couchrest/validation/auto_validate.rb +163 -0
  46. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  47. data/lib/couchrest/validation/validation_errors.rb +118 -0
  48. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  49. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  50. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  51. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  52. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  53. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  54. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  55. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  56. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  57. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  58. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  59. data/spec/couchrest/core/database_spec.rb +694 -0
  60. data/spec/couchrest/core/design_spec.rb +138 -0
  61. data/spec/couchrest/core/document_spec.rb +267 -0
  62. data/spec/couchrest/core/server_spec.rb +65 -0
  63. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  64. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  65. data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
  66. data/spec/couchrest/more/casted_model_spec.rb +98 -0
  67. data/spec/couchrest/more/extended_doc_attachment_spec.rb +130 -0
  68. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  69. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  70. data/spec/couchrest/more/extended_doc_view_spec.rb +355 -0
  71. data/spec/couchrest/more/property_spec.rb +136 -0
  72. data/spec/fixtures/attachments/README +3 -0
  73. data/spec/fixtures/attachments/couchdb.png +0 -0
  74. data/spec/fixtures/attachments/test.html +11 -0
  75. data/spec/fixtures/more/article.rb +34 -0
  76. data/spec/fixtures/more/card.rb +20 -0
  77. data/spec/fixtures/more/course.rb +14 -0
  78. data/spec/fixtures/more/event.rb +6 -0
  79. data/spec/fixtures/more/invoice.rb +17 -0
  80. data/spec/fixtures/more/person.rb +8 -0
  81. data/spec/fixtures/more/question.rb +6 -0
  82. data/spec/fixtures/more/service.rb +12 -0
  83. data/spec/fixtures/views/lib.js +3 -0
  84. data/spec/fixtures/views/test_view/lib.js +3 -0
  85. data/spec/fixtures/views/test_view/only-map.js +4 -0
  86. data/spec/fixtures/views/test_view/test-map.js +3 -0
  87. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  88. data/spec/spec.opts +6 -0
  89. data/spec/spec_helper.rb +26 -0
  90. data/utils/remap.rb +27 -0
  91. data/utils/subset.rb +30 -0
  92. metadata +200 -0
@@ -0,0 +1,87 @@
1
+ require 'delegate'
2
+
3
+ module CouchRest
4
+ class Document < Response
5
+ include CouchRest::Mixins::Attachments
6
+
7
+ # def self.inherited(subklass)
8
+ # subklass.send(:extlib_inheritable_accessor, :database)
9
+ # end
10
+
11
+ extlib_inheritable_accessor :database
12
+ attr_accessor :database
13
+
14
+ # override the CouchRest::Model-wide default_database
15
+ # This is not a thread safe operation, do not change the model
16
+ # database at runtime.
17
+ def self.use_database(db)
18
+ self.database = db
19
+ end
20
+
21
+ def id
22
+ self['_id']
23
+ end
24
+
25
+ def rev
26
+ self['_rev']
27
+ end
28
+
29
+ # returns true if the document has never been saved
30
+ def new_document?
31
+ !rev
32
+ end
33
+
34
+ # Saves the document to the db using create or update. Also runs the :save
35
+ # callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on
36
+ # CouchDB's response.
37
+ # If <tt>bulk</tt> is <tt>true</tt> (defaults to false) the document is cached for bulk save.
38
+ def save(bulk = false)
39
+ raise ArgumentError, "doc.database required for saving" unless database
40
+ result = database.save_doc self, bulk
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
+ # If <tt>bulk</tt> is <tt>true</tt> (defaults to false) the document won't
48
+ # actually be deleted from the db until bulk save.
49
+ def destroy(bulk = false)
50
+ raise ArgumentError, "doc.database required to destroy" unless database
51
+ result = database.delete_doc(self, bulk)
52
+ if result['ok']
53
+ self['_rev'] = nil
54
+ self['_id'] = nil
55
+ end
56
+ result['ok']
57
+ end
58
+
59
+ # copies the document to a new id. If the destination id currently exists, a rev must be provided.
60
+ # <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
61
+ # hash with a '_rev' key
62
+ def copy(dest)
63
+ raise ArgumentError, "doc.database required to copy" unless database
64
+ result = database.copy_doc(self, dest)
65
+ result['ok']
66
+ end
67
+
68
+ # Returns the CouchDB uri for the document
69
+ def uri(append_rev = false)
70
+ return nil if new_document?
71
+ couch_uri = "http://#{database.uri}/#{CGI.escape(id)}"
72
+ if append_rev == true
73
+ couch_uri << "?rev=#{rev}"
74
+ elsif append_rev.kind_of?(Integer)
75
+ couch_uri << "?rev=#{append_rev}"
76
+ end
77
+ couch_uri
78
+ end
79
+
80
+ # Returns the document's database
81
+ def database
82
+ @database || self.class.database
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -0,0 +1,16 @@
1
+ module CouchRest
2
+ class Response < Hash
3
+ def initialize(pkeys = {})
4
+ pkeys ||= {}
5
+ pkeys.each do |k,v|
6
+ self[k.to_s] = v
7
+ end
8
+ end
9
+ def []=(key, value)
10
+ super(key.to_s, value)
11
+ end
12
+ def [](key)
13
+ super(key.to_s)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,103 @@
1
+ module CouchRest
2
+ class Server
3
+ attr_accessor :uri, :uuid_batch_count, :available_databases
4
+ def initialize(server = 'http://127.0.0.1:5984', uuid_batch_count = 1000)
5
+ case server
6
+ when %r{\A(http://[^/]+)/(.*)\z}
7
+ @uri, @prefix = $1, $2
8
+ else
9
+ @uri, @prefix = server, ""
10
+ end
11
+ @uuid_batch_count = uuid_batch_count
12
+ end
13
+
14
+ # Add default prefix to database name
15
+ def expand(name)
16
+ @prefix + name
17
+ end
18
+
19
+ # Lists all "available" databases.
20
+ # An available database, is a database that was specified
21
+ # as avaiable by your code.
22
+ # It allows to define common databases to use and reuse in your code
23
+ def available_databases
24
+ @available_databases ||= {}
25
+ end
26
+
27
+ # Adds a new available database and create it unless it already exists
28
+ #
29
+ # Example:
30
+ #
31
+ # @couch = CouchRest::Server.new
32
+ # @couch.define_available_database(:default, "tech-blog")
33
+ #
34
+ def define_available_database(reference, db_name, create_unless_exists = true)
35
+ available_databases[reference.to_sym] = create_unless_exists ? database!(db_name) : database(db_name)
36
+ end
37
+
38
+ # Checks that a database is set as available
39
+ #
40
+ # Example:
41
+ #
42
+ # @couch.available_database?(:default)
43
+ #
44
+ def available_database?(ref_or_name)
45
+ ref_or_name.is_a?(Symbol) ? available_databases.keys.include?(ref_or_name) : available_databases.values.map{|db| db.name}.include?(expand(ref_or_name))
46
+ end
47
+
48
+ def default_database=(name, create_unless_exists = true)
49
+ define_available_database(:default, name, create_unless_exists = true)
50
+ end
51
+
52
+ def default_database
53
+ available_databases[:default]
54
+ end
55
+
56
+ # Lists all databases on the server
57
+ def databases
58
+ dbs = CouchRest.get "#{@uri}/_all_dbs"
59
+ unless @prefix.empty?
60
+ pfx = @prefix.gsub('/','%2F')
61
+ dbs.reject! { |db| db.index(pfx) != 0 }
62
+ end
63
+ dbs
64
+ end
65
+
66
+ # Returns a CouchRest::Database for the given name
67
+ def database(name)
68
+ CouchRest::Database.new(self, expand(name))
69
+ end
70
+
71
+ # Creates the database if it doesn't exist
72
+ def database!(name)
73
+ create_db(name) rescue nil
74
+ database(name)
75
+ end
76
+
77
+ # GET the welcome message
78
+ def info
79
+ CouchRest.get "#{@uri}/"
80
+ end
81
+
82
+ # Create a database
83
+ def create_db(name)
84
+ CouchRest.put "#{@uri}/#{expand(name).gsub('/','%2F')}"
85
+ database(name)
86
+ end
87
+
88
+ # Restart the CouchDB instance
89
+ def restart!
90
+ CouchRest.post "#{@uri}/_restart"
91
+ end
92
+
93
+ # Retrive an unused UUID from CouchDB. Server instances manage caching a list of unused UUIDs.
94
+ def next_uuid(count = @uuid_batch_count)
95
+ @uuids ||= []
96
+ if @uuids.empty?
97
+ @uuids = CouchRest.get("#{@uri}/_uuids?count=#{count}")["uuids"]
98
+ end
99
+ @uuids.pop
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,4 @@
1
+ module CouchRest
2
+ class View
3
+ end
4
+ end
@@ -0,0 +1,103 @@
1
+ module CouchRest
2
+ class Pager
3
+ attr_accessor :db
4
+ def initialize db
5
+ @db = db
6
+ end
7
+
8
+ def all_docs(limit=100, &block)
9
+ startkey = nil
10
+ oldend = nil
11
+
12
+ while docrows = request_all_docs(limit+1, startkey)
13
+ startkey = docrows.last['key']
14
+ docrows.pop if docrows.length > limit
15
+ if oldend == startkey
16
+ break
17
+ end
18
+ yield(docrows)
19
+ oldend = startkey
20
+ end
21
+ end
22
+
23
+ def key_reduce(view, limit=2000, firstkey = nil, lastkey = nil, &block)
24
+ # start with no keys
25
+ startkey = firstkey
26
+ # lastprocessedkey = nil
27
+ keepgoing = true
28
+
29
+ while keepgoing && viewrows = request_view(view, limit, startkey)
30
+ startkey = viewrows.first['key']
31
+ endkey = viewrows.last['key']
32
+
33
+ if (startkey == endkey)
34
+ # we need to rerequest to get a bigger page
35
+ # so we know we have all the rows for that key
36
+ viewrows = @db.view(view, :key => startkey)['rows']
37
+ # we need to do an offset thing to find the next startkey
38
+ # otherwise we just get stuck
39
+ lastdocid = viewrows.last['id']
40
+ fornextloop = @db.view(view, :startkey => startkey, :startkey_docid => lastdocid, :limit => 2)['rows']
41
+
42
+ newendkey = fornextloop.last['key']
43
+ if (newendkey == endkey)
44
+ keepgoing = false
45
+ else
46
+ startkey = newendkey
47
+ end
48
+ rows = viewrows
49
+ else
50
+ rows = []
51
+ for r in viewrows
52
+ if (lastkey && r['key'] == lastkey)
53
+ keepgoing = false
54
+ break
55
+ end
56
+ break if (r['key'] == endkey)
57
+ rows << r
58
+ end
59
+ startkey = endkey
60
+ end
61
+
62
+ key = :begin
63
+ values = []
64
+
65
+ rows.each do |r|
66
+ if key != r['key']
67
+ # we're on a new key, yield the old first and then reset
68
+ yield(key, values) if key != :begin
69
+ key = r['key']
70
+ values = []
71
+ end
72
+ # keep accumulating
73
+ values << r['value']
74
+ end
75
+ yield(key, values)
76
+
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def request_all_docs limit, startkey = nil
83
+ opts = {}
84
+ opts[:limit] = limit if limit
85
+ opts[:startkey] = startkey if startkey
86
+ results = @db.documents(opts)
87
+ rows = results['rows']
88
+ rows unless rows.length == 0
89
+ end
90
+
91
+ def request_view view, limit = nil, startkey = nil, endkey = nil
92
+ opts = {}
93
+ opts[:limit] = limit if limit
94
+ opts[:startkey] = startkey if startkey
95
+ opts[:endkey] = endkey if endkey
96
+
97
+ results = @db.view(view, opts)
98
+ rows = results['rows']
99
+ rows unless rows.length == 0
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,44 @@
1
+ module CouchRest
2
+ class Streamer
3
+ attr_accessor :db
4
+ def initialize db
5
+ @db = db
6
+ end
7
+
8
+ # Stream a view, yielding one row at a time. Shells out to <tt>curl</tt> to keep RAM usage low when you have millions of rows.
9
+ def view name, params = nil, &block
10
+ urlst = /^_/.match(name) ? "#{@db.root}/#{name}" : "#{@db.root}/_view/#{name}"
11
+ url = CouchRest.paramify_url urlst, params
12
+ # puts "stream #{url}"
13
+ first = nil
14
+ IO.popen("curl --silent #{url}") do |view|
15
+ first = view.gets # discard header
16
+ while line = view.gets
17
+ row = parse_line(line)
18
+ block.call row
19
+ end
20
+ end
21
+ parse_first(first)
22
+ end
23
+
24
+ private
25
+
26
+ def parse_line line
27
+ return nil unless line
28
+ if /(\{.*\}),?/.match(line.chomp)
29
+ JSON.parse($1)
30
+ end
31
+ end
32
+
33
+ def parse_first first
34
+ return nil unless first
35
+ parts = first.split(',')
36
+ parts.pop
37
+ line = parts.join(',')
38
+ JSON.parse("#{line}}")
39
+ rescue
40
+ nil
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ module CouchRest
2
+ class Upgrade
3
+ attr_accessor :olddb, :newdb, :dbname
4
+ def initialize dbname, old_couch, new_couch
5
+ @dbname = dbname
6
+ @olddb = old_couch.database dbname
7
+ @newdb = new_couch.database!(dbname)
8
+ @bulk_docs = []
9
+ end
10
+ def clone!
11
+ puts "#{dbname} - #{olddb.info['doc_count']} docs"
12
+ streamer = CouchRest::Streamer.new(olddb)
13
+ streamer.view("_all_docs_by_seq") do |row|
14
+ load_row_docs(row) if row
15
+ maybe_flush_bulks
16
+ end
17
+ flush_bulks!
18
+ end
19
+
20
+ private
21
+
22
+ def maybe_flush_bulks
23
+ flush_bulks! if (@bulk_docs.length > 99)
24
+ end
25
+
26
+ def flush_bulks!
27
+ url = CouchRest.paramify_url "#{@newdb.uri}/_bulk_docs", {:all_or_nothing => true}
28
+ puts "posting #{@bulk_docs.length} bulk docs to #{url}"
29
+ begin
30
+ CouchRest.post url, {:docs => @bulk_docs}
31
+ @bulk_docs = []
32
+ rescue Exception => e
33
+ puts e.response
34
+ raise e
35
+ end
36
+ end
37
+
38
+ def load_row_docs(row)
39
+ results = @olddb.get(row["id"], {:open_revs => "all", :attachments => true})
40
+ results.select{|r|r["ok"]}.each do |r|
41
+ doc = r["ok"]
42
+ if /^_/.match(doc["_id"]) && !/^_design/.match(doc["_id"])
43
+ puts "invalid docid #{doc["_id"]} -- trimming"
44
+ doc["_id"] = doc["_id"].sub('_','')
45
+ end
46
+ doc.delete('_rev')
47
+ @bulk_docs << doc
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,4 @@
1
+ mixins_dir = File.join(File.dirname(__FILE__), 'mixins')
2
+
3
+ require File.join(mixins_dir, 'attachments')
4
+ require File.join(mixins_dir, 'callbacks')
@@ -0,0 +1,31 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module Attachments
4
+
5
+ # saves an attachment directly to couchdb
6
+ def put_attachment(name, file, options={})
7
+ raise ArgumentError, "doc must be saved" unless self.rev
8
+ raise ArgumentError, "doc.database required to put_attachment" unless database
9
+ result = database.put_attachment(self, name, file, options)
10
+ self['_rev'] = result['rev']
11
+ result['ok']
12
+ end
13
+
14
+ # returns an attachment's data
15
+ def fetch_attachment(name)
16
+ raise ArgumentError, "doc must be saved" unless self.rev
17
+ raise ArgumentError, "doc.database required to put_attachment" unless database
18
+ database.fetch_attachment(self, name)
19
+ end
20
+
21
+ # deletes an attachment directly from couchdb
22
+ def delete_attachment(name)
23
+ raise ArgumentError, "doc.database required to delete_attachment" unless database
24
+ result = database.delete_attachment(self, name)
25
+ self['_rev'] = result['rev']
26
+ result['ok']
27
+ end
28
+
29
+ end
30
+ end
31
+ end