couchrest 0.35 → 0.36

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/Rakefile CHANGED
@@ -20,7 +20,7 @@ begin
20
20
  gemspec.description = "CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments."
21
21
  gemspec.email = "jchris@apache.org"
22
22
  gemspec.homepage = "http://github.com/couchrest/couchrest"
23
- gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos"]
23
+ gemspec.authors = ["J. Chris Anderson", "Matt Aimonetti", "Marcos Tapajos", "Will Leinweber"]
24
24
  gemspec.extra_rdoc_files = %w( README.md LICENSE THANKS.md )
25
25
  gemspec.files = %w( LICENSE README.md Rakefile THANKS.md history.txt) + Dir["{examples,lib,spec,utils}/**/*"] - Dir["spec/tmp"]
26
26
  gemspec.has_rdoc = true
@@ -64,4 +64,4 @@ module Rake
64
64
  end
65
65
 
66
66
  Rake.remove_task("github:release")
67
- Rake.remove_task("release")
67
+ Rake.remove_task("release")
@@ -4,6 +4,16 @@
4
4
 
5
5
  * Minor enhancements
6
6
 
7
+ == 0.36
8
+
9
+ * Major enhancements
10
+ * Adds support for continuous replication (sauy7)
11
+ * Automatic Type Casting (Alexander Uvarov, Sam Lown, Tim Heighes, Will Leinweber)
12
+ * Added a search method to CouchRest:Database to search the documents in a given database. (Dave Farkas, Arnaud Berthomier, John Wood)
13
+
14
+ * Minor enhancements
15
+ * Provide a description of the timeout error (John Wood)
16
+
7
17
  == 0.35
8
18
 
9
19
  * Major enhancements
@@ -28,7 +28,7 @@ require 'couchrest/monkeypatches'
28
28
 
29
29
  # = CouchDB, close to the metal
30
30
  module CouchRest
31
- VERSION = '0.35' unless self.const_defined?("VERSION")
31
+ VERSION = '0.36' unless self.const_defined?("VERSION")
32
32
 
33
33
  autoload :Server, 'couchrest/core/server'
34
34
  autoload :Database, 'couchrest/core/database'
@@ -96,15 +96,18 @@ module CouchRest
96
96
 
97
97
  def parse url
98
98
  case url
99
- when /^https?:\/\/(.*)\/(.*)\/(.*)/
100
- host = $1
101
- db = $2
102
- docid = $3
103
- when /^https?:\/\/(.*)\/(.*)/
104
- host = $1
105
- db = $2
106
- when /^https?:\/\/(.*)/
107
- host = $1
99
+ when /^(https?:\/\/)(.*)\/(.*)\/(.*)/
100
+ scheme = $1
101
+ host = $2
102
+ db = $3
103
+ docid = $4
104
+ when /^(https?:\/\/)(.*)\/(.*)/
105
+ scheme = $1
106
+ host = $2
107
+ db = $3
108
+ when /^(https?:\/\/)(.*)/
109
+ scheme = $1
110
+ host = $2
108
111
  when /(.*)\/(.*)\/(.*)/
109
112
  host = $1
110
113
  db = $2
@@ -117,9 +120,9 @@ module CouchRest
117
120
  end
118
121
 
119
122
  db = nil if db && db.empty?
120
-
123
+
121
124
  {
122
- :host => host || "127.0.0.1:5984",
125
+ :host => (scheme || "http://") + (host || "127.0.0.1:5984"),
123
126
  :database => db,
124
127
  :doc => docid
125
128
  }
@@ -44,7 +44,14 @@ module CouchRest
44
44
  CouchRest.get url
45
45
  end
46
46
  end
47
-
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
+
48
55
  # load a set of documents by passing an array of ids
49
56
  def get_bulk(ids)
50
57
  documents(:keys => ids, :include_docs => true)
@@ -297,15 +304,13 @@ module CouchRest
297
304
  end
298
305
 
299
306
  # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts.
300
- def replicate_from other_db
301
- raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
302
- CouchRest.post "#{@host}/_replicate", :source => other_db.root, :target => name
307
+ def replicate_from other_db, continuous=false
308
+ replicate other_db, continuous, :target => name
303
309
  end
304
310
 
305
311
  # Replicates via "pushing" to another database. Makes no attempt to deal with conflicts.
306
- def replicate_to other_db
307
- raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
308
- CouchRest.post "#{@host}/_replicate", :target => other_db.root, :source => name
312
+ def replicate_to other_db, continuous=false
313
+ replicate other_db, continuous, :source => name
309
314
  end
310
315
 
311
316
  # DELETE the database itself. This is not undoable and could be rather
@@ -317,6 +322,19 @@ module CouchRest
317
322
 
318
323
  private
319
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
+
320
338
  def clear_extended_doc_fresh_cache
321
339
  ::CouchRest::ExtendedDocument.subclasses.each{|klass| klass.design_doc_fresh = false if klass.respond_to?(:design_doc_fresh=) }
322
340
  end
@@ -20,7 +20,7 @@ module CouchRest
20
20
  class_eval <<-END, __FILE__, __LINE__ + 1
21
21
  def self.find_all_#{collection_name}(options = {})
22
22
  view_options = #{view_options.inspect} || {}
23
- CollectionProxy.new(@database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
23
+ CollectionProxy.new(database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
24
24
  end
25
25
  END
26
26
  end
@@ -42,7 +42,12 @@ module CouchRest
42
42
  #
43
43
  # Defaults are used if these options are not specified.
44
44
  def paginated_each(options, &block)
45
- proxy = create_collection_proxy(options)
45
+ search = options.delete(:search)
46
+ unless search == true
47
+ proxy = create_collection_proxy(options)
48
+ else
49
+ proxy = create_search_collection_proxy(options)
50
+ end
46
51
  proxy.paginated_each(options, &block)
47
52
  end
48
53
 
@@ -58,7 +63,12 @@ module CouchRest
58
63
 
59
64
  def create_collection_proxy(options)
60
65
  design_doc, view_name, view_options = parse_view_options(options)
61
- CollectionProxy.new(@database, design_doc, view_name, view_options, self)
66
+ CollectionProxy.new(database, design_doc, view_name, view_options, self)
67
+ end
68
+
69
+ def create_search_collection_proxy(options)
70
+ design_doc, search_name, search_options = parse_search_options(options)
71
+ CollectionProxy.new(@database, design_doc, search_name, search_options, self, :search)
62
72
  end
63
73
 
64
74
  def parse_view_options(options)
@@ -75,6 +85,18 @@ module CouchRest
75
85
 
76
86
  [design_doc, view_name, view_options]
77
87
  end
88
+
89
+ def parse_search_options(options)
90
+ design_doc = options.delete(:design_doc)
91
+ raise ArgumentError, 'design_doc is required' if design_doc.nil?
92
+
93
+ search_name = options.delete(:view_name)
94
+ raise ArgumentError, 'search_name is required' if search_name.nil?
95
+
96
+ search_options = options.clone
97
+ [design_doc, search_name, search_options]
98
+ end
99
+
78
100
  end
79
101
 
80
102
  class CollectionProxy
@@ -91,11 +113,12 @@ module CouchRest
91
113
  #
92
114
  # The CollectionProxy provides support for paginating over a collection
93
115
  # via the paginate, and paginated_each methods.
94
- def initialize(database, design_doc, view_name, view_options = {}, container_class = nil)
116
+ def initialize(database, design_doc, view_name, view_options = {}, container_class = nil, query_type = :view)
95
117
  raise ArgumentError, "database is a required parameter" if database.nil?
96
118
 
97
119
  @database = database
98
120
  @container_class = container_class
121
+ @query_type = query_type
99
122
 
100
123
  strip_pagination_options(view_options)
101
124
  @view_options = view_options
@@ -110,10 +133,22 @@ module CouchRest
110
133
  # See Collection.paginate
111
134
  def paginate(options = {})
112
135
  page, per_page = parse_options(options)
113
- results = @database.view(@view_name, pagination_options(page, per_page))
136
+ results = @database.send(@query_type, @view_name, pagination_options(page, per_page))
114
137
  remember_where_we_left_off(results, page)
115
- results = convert_to_container_array(results)
116
- results
138
+ instances = convert_to_container_array(results)
139
+
140
+ begin
141
+ if Kernel.const_get('WillPaginate')
142
+ total_rows = results['total_rows'].to_i
143
+ paginated = WillPaginate::Collection.create(page, per_page, total_rows) do |pager|
144
+ pager.replace(instances)
145
+ end
146
+ return paginated
147
+ end
148
+ rescue NameError
149
+ # When not using will_paginate, not much we could do about this. :x
150
+ end
151
+ return instances
117
152
  end
118
153
 
119
154
  # See Collection.paginated_each
@@ -152,7 +187,8 @@ module CouchRest
152
187
 
153
188
  def load_target
154
189
  unless loaded?
155
- results = @database.view(@view_name, @view_options)
190
+ @view_options.merge!({:include_docs => true}) if @query_type == :search
191
+ results = @database.send(@query_type, @view_name, @view_options)
156
192
  @target = convert_to_container_array(results)
157
193
  end
158
194
  @loaded = true
@@ -189,7 +225,7 @@ module CouchRest
189
225
 
190
226
  def pagination_options(page, per_page)
191
227
  view_options = @view_options.clone
192
- if @last_key && @last_docid && @last_page == page - 1
228
+ if @query_type == :view && @last_key && @last_docid && @last_page == page - 1
193
229
  key = view_options.delete(:key)
194
230
  end_key = view_options[:endkey] || key
195
231
  options = { :startkey => @last_key, :endkey => end_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
@@ -1,24 +1,6 @@
1
1
  require 'time'
2
2
  require File.join(File.dirname(__FILE__), '..', 'more', 'property')
3
-
4
- class Time
5
- # returns a local time value much faster than Time.parse
6
- def self.mktime_with_offset(string)
7
- string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/
8
- # $1 = year
9
- # $2 = month
10
- # $3 = day
11
- # $4 = hours
12
- # $5 = minutes
13
- # $6 = seconds
14
- # $7 = time zone direction
15
- # $8 = tz difference
16
- # utc time with wrong TZ info:
17
- time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7)
18
- tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600)
19
- time + tz_difference + zone_offset(time.zone)
20
- end
21
- end
3
+ require File.join(File.dirname(__FILE__), '..', 'more', 'typecast')
22
4
 
23
5
  module CouchRest
24
6
  module Mixins
@@ -26,6 +8,8 @@ module CouchRest
26
8
 
27
9
  class IncludeError < StandardError; end
28
10
 
11
+ include ::CouchRest::More::Typecast
12
+
29
13
  def self.included(base)
30
14
  base.class_eval <<-EOS, __FILE__, __LINE__ + 1
31
15
  extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
@@ -67,28 +51,24 @@ module CouchRest
67
51
  return unless self[key]
68
52
  if property.type.is_a?(Array)
69
53
  klass = ::CouchRest.constantize(property.type[0])
70
- arr = self[key].dup.collect do |value|
71
- unless value.instance_of?(klass)
72
- value = convert_property_value(property, klass, value)
73
- end
54
+ self[key] = [self[key]] unless self[key].is_a?(Array)
55
+ arr = self[key].collect do |value|
56
+ value = typecast_value(value, klass, property.init_method)
74
57
  associate_casted_to_parent(value, assigned)
75
58
  value
76
59
  end
77
60
  self[key] = klass != String ? CastedArray.new(arr) : arr
78
61
  self[key].casted_by = self if self[key].respond_to?(:casted_by)
79
62
  else
80
- if property.type == 'boolean'
63
+ if property.type.downcase == 'boolean'
81
64
  klass = TrueClass
82
65
  else
83
66
  klass = ::CouchRest.constantize(property.type)
84
67
  end
85
68
 
86
- unless self[key].instance_of?(klass)
87
- self[key] = convert_property_value(property, klass, self[property.name])
88
- end
89
- associate_casted_to_parent(self[property.name], assigned)
69
+ self[key] = typecast_value(self[key], klass, property.init_method)
70
+ associate_casted_to_parent(self[key], assigned)
90
71
  end
91
-
92
72
  end
93
73
 
94
74
  def associate_casted_to_parent(casted, assigned)
@@ -96,21 +76,6 @@ module CouchRest
96
76
  casted.document_saved = true if !assigned && casted.respond_to?(:document_saved)
97
77
  end
98
78
 
99
- def convert_property_value(property, klass, value)
100
- if ((property.init_method == 'new') && klass == Time)
101
- # Using custom time parsing method because Ruby's default method is toooo slow
102
- value.is_a?(String) ? Time.mktime_with_offset(value.dup) : value
103
- # Float instances don't get initialized with #new
104
- elsif ((property.init_method == 'new') && klass == Float)
105
- cast_float(value)
106
- # 'boolean' type is simply used to generate a property? accessor method
107
- elsif ((property.init_method == 'new') && klass == TrueClass)
108
- value
109
- else
110
- klass.send(property.init_method, value.dup)
111
- end
112
- end
113
-
114
79
  def cast_property_by_name(property_name)
115
80
  return unless self.class.properties
116
81
  property = self.class.properties.detect{|property| property.name == property_name}
@@ -118,14 +83,7 @@ module CouchRest
118
83
  cast_property(property, true)
119
84
  end
120
85
 
121
- def cast_float(value)
122
- begin
123
- Float(value)
124
- rescue
125
- value
126
- end
127
- end
128
-
86
+
129
87
  module ClassMethods
130
88
 
131
89
  def property(name, options={})
@@ -141,7 +99,7 @@ module CouchRest
141
99
  # make sure to use a mutex.
142
100
  def define_property(name, options={})
143
101
  # check if this property is going to casted
144
- options[:casted] = options[:cast_as] ? options[:cast_as] : false
102
+ options[:casted] = !!(options[:cast_as] || options[:type])
145
103
  property = CouchRest::Property.new(name, (options.delete(:cast_as) || options.delete(:type)), options)
146
104
  create_property_getter(property)
147
105
  create_property_setter(property) unless property.read_only == true
@@ -39,7 +39,7 @@ if RUBY_VERSION.to_f < 1.9
39
39
  if IO.select([@io], nil, nil, @read_timeout)
40
40
  retry
41
41
  else
42
- raise Timeout::Error
42
+ raise Timeout::Error, "IO timeout"
43
43
  end
44
44
  end
45
45
  else
@@ -14,7 +14,7 @@ module CouchRest
14
14
  include CouchRest::Mixins::ExtendedAttachments
15
15
  include CouchRest::Mixins::ClassProxy
16
16
  include CouchRest::Mixins::Collection
17
- include CouchRest::Mixins::AttributeProtection
17
+ include CouchRest::Mixins::AttributeProtection
18
18
 
19
19
  def self.subclasses
20
20
  @subclasses ||= []
@@ -58,6 +58,7 @@ module CouchRest
58
58
  unless self['_id'] && self['_rev']
59
59
  self['couchrest-type'] = self.class.to_s
60
60
  end
61
+ after_initialize if respond_to?(:after_initialize)
61
62
  end
62
63
 
63
64
  # Defines an instance and save it directly to the database
@@ -84,9 +85,9 @@ module CouchRest
84
85
  # on the document whenever saving occurs. CouchRest uses a pretty
85
86
  # decent time format by default. See Time#to_json
86
87
  def self.timestamps!
87
- class_eval <<-EOS, __FILE__, __LINE__ + 1
88
- property(:updated_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
89
- property(:created_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
88
+ class_eval <<-EOS, __FILE__, __LINE__
89
+ property(:updated_at, :read_only => true, :type => 'Time', :auto_validation => false)
90
+ property(:created_at, :read_only => true, :type => 'Time', :auto_validation => false)
90
91
 
91
92
  set_callback :save, :before do |object|
92
93
  object['updated_at'] = Time.now
@@ -1,9 +1,9 @@
1
1
  module CouchRest
2
-
2
+
3
3
  # Basic attribute support for adding getter/setter + validation
4
4
  class Property
5
5
  attr_reader :name, :type, :read_only, :alias, :default, :casted, :init_method, :options
6
-
6
+
7
7
  # attribute to define
8
8
  def initialize(name, type = nil, options = {})
9
9
  @name = name.to_s
@@ -11,20 +11,19 @@ module CouchRest
11
11
  parse_options(options)
12
12
  self
13
13
  end
14
-
15
-
14
+
16
15
  private
17
-
16
+
18
17
  def parse_type(type)
19
18
  if type.nil?
20
19
  @type = 'String'
21
20
  elsif type.is_a?(Array) && type.empty?
22
- @type = 'Array'
21
+ @type = ['Object']
23
22
  else
24
23
  @type = type.is_a?(Array) ? [type.first.to_s] : type.to_s
25
24
  end
26
25
  end
27
-
26
+
28
27
  def parse_options(options)
29
28
  return if options.empty?
30
29
  @validation_format = options.delete(:format) if options[:format]
@@ -35,7 +34,7 @@ module CouchRest
35
34
  @init_method = options[:init_method] ? options.delete(:init_method) : 'new'
36
35
  @options = options
37
36
  end
38
-
37
+
39
38
  end
40
39
  end
41
40
 
@@ -56,4 +55,4 @@ class CastedArray < Array
56
55
  obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
57
56
  super(index, obj)
58
57
  end
59
- end
58
+ end