couchrest 0.35 → 0.36
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +2 -2
- data/history.txt +10 -0
- data/lib/couchrest.rb +15 -12
- data/lib/couchrest/core/database.rb +25 -7
- data/lib/couchrest/mixins/collection.rb +45 -9
- data/lib/couchrest/mixins/properties.rb +11 -53
- data/lib/couchrest/monkeypatches.rb +1 -1
- data/lib/couchrest/more/extended_document.rb +5 -4
- data/lib/couchrest/more/property.rb +8 -9
- data/lib/couchrest/more/typecast.rb +180 -0
- data/lib/couchrest/validation/validators/required_field_validator.rb +2 -2
- data/spec/couchrest/core/couchrest_spec.rb +16 -79
- data/spec/couchrest/core/database_spec.rb +70 -3
- data/spec/couchrest/more/casted_model_spec.rb +1 -1
- data/spec/couchrest/more/extended_doc_spec.rb +58 -7
- data/spec/couchrest/more/property_spec.rb +424 -85
- data/spec/fixtures/more/article.rb +2 -2
- data/spec/fixtures/more/course.rb +13 -5
- data/spec/fixtures/more/event.rb +1 -2
- data/spec/fixtures/more/person.rb +1 -1
- data/spec/fixtures/more/question.rb +1 -1
- data/spec/fixtures/more/service.rb +1 -1
- data/spec/spec_helper.rb +13 -1
- metadata +27 -13
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")
|
data/history.txt
CHANGED
@@ -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
|
data/lib/couchrest.rb
CHANGED
@@ -28,7 +28,7 @@ require 'couchrest/monkeypatches'
|
|
28
28
|
|
29
29
|
# = CouchDB, close to the metal
|
30
30
|
module CouchRest
|
31
|
-
VERSION = '0.
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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(
|
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.
|
136
|
+
results = @database.send(@query_type, @view_name, pagination_options(page, per_page))
|
114
137
|
remember_where_we_left_off(results, page)
|
115
|
-
|
116
|
-
|
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
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
87
|
-
|
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
|
-
|
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]
|
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
|
@@ -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
|
-
|
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__
|
88
|
-
property(:updated_at, :read_only => true, :
|
89
|
-
property(:created_at, :read_only => true, :
|
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 = '
|
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
|