couchrest 0.35 → 0.36

Sign up to get free protection for your applications and to get access to all the features.
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