couchrest_extended_document 1.0.0.beta5
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/LICENSE +176 -0
- data/README.md +68 -0
- data/Rakefile +68 -0
- data/THANKS.md +19 -0
- data/examples/model/example.rb +144 -0
- data/history.txt +159 -0
- data/lib/couchrest/casted_array.rb +25 -0
- data/lib/couchrest/casted_model.rb +55 -0
- data/lib/couchrest/extended_document.rb +323 -0
- data/lib/couchrest/mixins/attribute_protection.rb +74 -0
- data/lib/couchrest/mixins/callbacks.rb +532 -0
- data/lib/couchrest/mixins/class_proxy.rb +120 -0
- data/lib/couchrest/mixins/collection.rb +260 -0
- data/lib/couchrest/mixins/design_doc.rb +127 -0
- data/lib/couchrest/mixins/document_queries.rb +82 -0
- data/lib/couchrest/mixins/extended_attachments.rb +73 -0
- data/lib/couchrest/mixins/properties.rb +162 -0
- data/lib/couchrest/mixins/validation.rb +245 -0
- data/lib/couchrest/mixins/views.rb +148 -0
- data/lib/couchrest/mixins.rb +11 -0
- data/lib/couchrest/property.rb +50 -0
- data/lib/couchrest/support/couchrest.rb +19 -0
- data/lib/couchrest/support/rails.rb +42 -0
- data/lib/couchrest/typecast.rb +175 -0
- data/lib/couchrest/validation/auto_validate.rb +156 -0
- data/lib/couchrest/validation/contextual_validators.rb +78 -0
- data/lib/couchrest/validation/validation_errors.rb +125 -0
- data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
- data/lib/couchrest/validation/validators/confirmation_validator.rb +107 -0
- data/lib/couchrest/validation/validators/format_validator.rb +122 -0
- data/lib/couchrest/validation/validators/formats/email.rb +66 -0
- data/lib/couchrest/validation/validators/formats/url.rb +43 -0
- data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
- data/lib/couchrest/validation/validators/length_validator.rb +139 -0
- data/lib/couchrest/validation/validators/method_validator.rb +89 -0
- data/lib/couchrest/validation/validators/numeric_validator.rb +109 -0
- data/lib/couchrest/validation/validators/required_field_validator.rb +114 -0
- data/lib/couchrest/validation.rb +245 -0
- data/lib/couchrest_extended_document.rb +21 -0
- data/spec/couchrest/attribute_protection_spec.rb +150 -0
- data/spec/couchrest/casted_extended_doc_spec.rb +79 -0
- data/spec/couchrest/casted_model_spec.rb +406 -0
- data/spec/couchrest/extended_doc_attachment_spec.rb +148 -0
- data/spec/couchrest/extended_doc_inherited_spec.rb +40 -0
- data/spec/couchrest/extended_doc_spec.rb +868 -0
- data/spec/couchrest/extended_doc_subclass_spec.rb +99 -0
- data/spec/couchrest/extended_doc_view_spec.rb +529 -0
- data/spec/couchrest/property_spec.rb +648 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/more/article.rb +35 -0
- data/spec/fixtures/more/card.rb +22 -0
- data/spec/fixtures/more/cat.rb +22 -0
- data/spec/fixtures/more/course.rb +25 -0
- data/spec/fixtures/more/event.rb +8 -0
- data/spec/fixtures/more/invoice.rb +17 -0
- data/spec/fixtures/more/person.rb +9 -0
- data/spec/fixtures/more/question.rb +6 -0
- data/spec/fixtures/more/service.rb +12 -0
- data/spec/fixtures/more/user.rb +22 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +49 -0
- data/utils/remap.rb +27 -0
- data/utils/subset.rb +30 -0
- metadata +200 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module Collection
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# Creates a new class method, find_all_<collection_name>, that will
|
12
|
+
# execute the view specified with the design_doc and view_name
|
13
|
+
# parameters, along with the specified view_options. This method will
|
14
|
+
# return the results of the view as an Array of objects which are
|
15
|
+
# instances of the class.
|
16
|
+
#
|
17
|
+
# This method is handy for objects that do not use the view_by method
|
18
|
+
# to declare their views.
|
19
|
+
def provides_collection(collection_name, design_doc, view_name, view_options)
|
20
|
+
class_eval <<-END, __FILE__, __LINE__ + 1
|
21
|
+
def self.find_all_#{collection_name}(options = {})
|
22
|
+
view_options = #{view_options.inspect} || {}
|
23
|
+
CollectionProxy.new(options[:database] || database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
|
24
|
+
end
|
25
|
+
END
|
26
|
+
end
|
27
|
+
|
28
|
+
# Fetch a group of objects from CouchDB. Options can include:
|
29
|
+
# :page - Specifies the page to load (starting at 1)
|
30
|
+
# :per_page - Specifies the number of objects to load per page
|
31
|
+
#
|
32
|
+
# Defaults are used if these options are not specified.
|
33
|
+
def paginate(options)
|
34
|
+
proxy = create_collection_proxy(options)
|
35
|
+
proxy.paginate(options)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Iterate over the objects in a collection, fetching them from CouchDB
|
39
|
+
# in groups. Options can include:
|
40
|
+
# :page - Specifies the page to load
|
41
|
+
# :per_page - Specifies the number of objects to load per page
|
42
|
+
#
|
43
|
+
# Defaults are used if these options are not specified.
|
44
|
+
def paginated_each(options, &block)
|
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
|
51
|
+
proxy.paginated_each(options, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Create a CollectionProxy for the specified view and options.
|
55
|
+
# CollectionProxy behaves just like an Array, but offers support for
|
56
|
+
# pagination.
|
57
|
+
def collection_proxy_for(design_doc, view_name, view_options = {})
|
58
|
+
options = view_options.merge(:design_doc => design_doc, :view_name => view_name)
|
59
|
+
create_collection_proxy(options)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def create_collection_proxy(options)
|
65
|
+
design_doc, view_name, view_options = parse_view_options(options)
|
66
|
+
CollectionProxy.new(options[:database] || 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(options[:database] || database, design_doc, search_name, search_options, self, :search)
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_view_options(options)
|
75
|
+
design_doc = options.delete(:design_doc)
|
76
|
+
raise ArgumentError, 'design_doc is required' if design_doc.nil?
|
77
|
+
|
78
|
+
view_name = options.delete(:view_name)
|
79
|
+
raise ArgumentError, 'view_name is required' if view_name.nil?
|
80
|
+
|
81
|
+
default_view_options = (design_doc.class == Design &&
|
82
|
+
design_doc['views'][view_name.to_s] &&
|
83
|
+
design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
|
84
|
+
view_options = default_view_options.merge(options)
|
85
|
+
|
86
|
+
[design_doc, view_name, view_options]
|
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
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
class CollectionProxy
|
103
|
+
alias_method :proxy_respond_to?, :respond_to?
|
104
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
|
105
|
+
|
106
|
+
DEFAULT_PAGE = 1
|
107
|
+
DEFAULT_PER_PAGE = 30
|
108
|
+
|
109
|
+
# Create a new CollectionProxy to represent the specified view. If a
|
110
|
+
# container class is specified, the proxy will create an object of the
|
111
|
+
# given type for each row that comes back from the view. If no
|
112
|
+
# container class is specified, the raw results are returned.
|
113
|
+
#
|
114
|
+
# The CollectionProxy provides support for paginating over a collection
|
115
|
+
# via the paginate, and paginated_each methods.
|
116
|
+
def initialize(database, design_doc, view_name, view_options = {}, container_class = nil, query_type = :view)
|
117
|
+
raise ArgumentError, "database is a required parameter" if database.nil?
|
118
|
+
|
119
|
+
@database = database
|
120
|
+
@container_class = container_class
|
121
|
+
@query_type = query_type
|
122
|
+
|
123
|
+
strip_pagination_options(view_options)
|
124
|
+
@view_options = view_options
|
125
|
+
|
126
|
+
if design_doc.class == Design
|
127
|
+
@view_name = "#{design_doc.name}/#{view_name}"
|
128
|
+
else
|
129
|
+
@view_name = "#{design_doc}/#{view_name}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# See Collection.paginate
|
134
|
+
def paginate(options = {})
|
135
|
+
page, per_page = parse_options(options)
|
136
|
+
results = @database.send(@query_type, @view_name, pagination_options(page, per_page))
|
137
|
+
remember_where_we_left_off(results, page)
|
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
|
152
|
+
end
|
153
|
+
|
154
|
+
# See Collection.paginated_each
|
155
|
+
def paginated_each(options = {}, &block)
|
156
|
+
page, per_page = parse_options(options)
|
157
|
+
|
158
|
+
begin
|
159
|
+
collection = paginate({:page => page, :per_page => per_page})
|
160
|
+
collection.each(&block)
|
161
|
+
page += 1
|
162
|
+
end until collection.size < per_page
|
163
|
+
end
|
164
|
+
|
165
|
+
def respond_to?(*args)
|
166
|
+
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
|
167
|
+
end
|
168
|
+
|
169
|
+
# Explicitly proxy === because the instance method removal above
|
170
|
+
# doesn't catch it.
|
171
|
+
def ===(other)
|
172
|
+
load_target
|
173
|
+
other === @target
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def method_missing(method, *args)
|
179
|
+
if load_target
|
180
|
+
if block_given?
|
181
|
+
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
182
|
+
else
|
183
|
+
@target.send(method, *args)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def load_target
|
189
|
+
unless loaded?
|
190
|
+
@view_options.merge!({:include_docs => true}) if @query_type == :search
|
191
|
+
results = @database.send(@query_type, @view_name, @view_options)
|
192
|
+
@target = convert_to_container_array(results)
|
193
|
+
end
|
194
|
+
@loaded = true
|
195
|
+
@target
|
196
|
+
end
|
197
|
+
|
198
|
+
def loaded?
|
199
|
+
@loaded
|
200
|
+
end
|
201
|
+
|
202
|
+
def reload
|
203
|
+
reset
|
204
|
+
load_target
|
205
|
+
self unless @target.nil?
|
206
|
+
end
|
207
|
+
|
208
|
+
def reset
|
209
|
+
@loaded = false
|
210
|
+
@target = nil
|
211
|
+
end
|
212
|
+
|
213
|
+
def inspect
|
214
|
+
load_target
|
215
|
+
@target.inspect
|
216
|
+
end
|
217
|
+
|
218
|
+
def convert_to_container_array(results)
|
219
|
+
if @container_class.nil?
|
220
|
+
results
|
221
|
+
else
|
222
|
+
results['rows'].collect { |row| @container_class.create_from_database(row['doc']) } unless results['rows'].nil?
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def pagination_options(page, per_page)
|
227
|
+
view_options = @view_options.clone
|
228
|
+
if @query_type == :view && @last_key && @last_docid && @last_page == page - 1
|
229
|
+
key = view_options.delete(:key)
|
230
|
+
end_key = view_options[:endkey] || key
|
231
|
+
options = { :startkey => @last_key, :endkey => end_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
|
232
|
+
else
|
233
|
+
options = { :limit => per_page, :skip => per_page * (page - 1) }
|
234
|
+
end
|
235
|
+
view_options.merge(options)
|
236
|
+
end
|
237
|
+
|
238
|
+
def parse_options(options)
|
239
|
+
page = options.delete(:page) || DEFAULT_PAGE
|
240
|
+
per_page = options.delete(:per_page) || DEFAULT_PER_PAGE
|
241
|
+
[page.to_i, per_page.to_i]
|
242
|
+
end
|
243
|
+
|
244
|
+
def strip_pagination_options(options)
|
245
|
+
parse_options(options)
|
246
|
+
end
|
247
|
+
|
248
|
+
def remember_where_we_left_off(results, page)
|
249
|
+
last_row = results['rows'].last
|
250
|
+
if last_row
|
251
|
+
@last_key = last_row['key']
|
252
|
+
@last_docid = last_row['id']
|
253
|
+
end
|
254
|
+
@last_page = page
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module CouchRest
|
4
|
+
module Mixins
|
5
|
+
module DesignDoc
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
def design_doc
|
14
|
+
@design_doc ||= Design.new(default_design_doc)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Use when something has been changed, like a view, so that on the next request
|
18
|
+
# the design docs will be updated (if changed!)
|
19
|
+
def req_design_doc_refresh
|
20
|
+
@design_doc_fresh = { }
|
21
|
+
end
|
22
|
+
|
23
|
+
def design_doc_id
|
24
|
+
"_design/#{design_doc_slug}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def design_doc_slug
|
28
|
+
self.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_design_doc
|
32
|
+
{
|
33
|
+
"_id" => design_doc_id,
|
34
|
+
"language" => "javascript",
|
35
|
+
"views" => {
|
36
|
+
'all' => {
|
37
|
+
'map' => "function(doc) {
|
38
|
+
if (doc['couchrest-type'] == '#{self.to_s}') {
|
39
|
+
emit(doc['_id'],1);
|
40
|
+
}
|
41
|
+
}"
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# DEPRECATED
|
48
|
+
# use stored_design_doc to retrieve the current design doc
|
49
|
+
def all_design_doc_versions(db = database)
|
50
|
+
db.documents :startkey => "_design/#{self.to_s}",
|
51
|
+
:endkey => "_design/#{self.to_s}-\u9999"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Retreive the latest version of the design document directly
|
55
|
+
# from the database.
|
56
|
+
def stored_design_doc(db = database)
|
57
|
+
db.get(design_doc_id) rescue nil
|
58
|
+
end
|
59
|
+
alias :model_design_doc :stored_design_doc
|
60
|
+
|
61
|
+
def refresh_design_doc(db = database)
|
62
|
+
raise "Database missing for design document refresh" if db.nil?
|
63
|
+
unless design_doc_fresh(db)
|
64
|
+
save_design_doc(db)
|
65
|
+
design_doc_fresh(db, true)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Save the design doc onto a target database in a thread-safe way,
|
70
|
+
# not modifying the model's design_doc
|
71
|
+
#
|
72
|
+
# See also save_design_doc! to always save the design doc even if there
|
73
|
+
# are no changes.
|
74
|
+
def save_design_doc(db = database, force = false)
|
75
|
+
update_design_doc(Design.new(design_doc), db, force)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Force the update of the model's design_doc even if it hasn't changed.
|
79
|
+
def save_design_doc!(db = database)
|
80
|
+
save_design_doc(db, true)
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
def design_doc_fresh(db, fresh = nil)
|
86
|
+
@design_doc_fresh ||= {}
|
87
|
+
if fresh.nil?
|
88
|
+
@design_doc_fresh[db.uri] || false
|
89
|
+
else
|
90
|
+
@design_doc_fresh[db.uri] = fresh
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Writes out a design_doc to a given database, returning the
|
95
|
+
# updated design doc
|
96
|
+
def update_design_doc(design_doc, db, force = false)
|
97
|
+
saved = stored_design_doc(db)
|
98
|
+
if saved
|
99
|
+
changes = force
|
100
|
+
design_doc['views'].each do |name, view|
|
101
|
+
if !compare_views(saved['views'][name], view)
|
102
|
+
changes = true
|
103
|
+
saved['views'][name] = view
|
104
|
+
end
|
105
|
+
end
|
106
|
+
if changes
|
107
|
+
db.save_doc(saved)
|
108
|
+
end
|
109
|
+
design_doc
|
110
|
+
else
|
111
|
+
design_doc.database = db
|
112
|
+
design_doc.save
|
113
|
+
design_doc
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Return true if the two views match
|
118
|
+
def compare_views(orig, repl)
|
119
|
+
return false if orig.nil? or repl.nil?
|
120
|
+
(orig['map'].to_s.strip == repl['map'].to_s.strip) && (orig['reduce'].to_s.strip == repl['reduce'].to_s.strip)
|
121
|
+
end
|
122
|
+
|
123
|
+
end # module ClassMethods
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module DocumentQueries
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# Load all documents that have the "couchrest-type" field equal to the
|
12
|
+
# name of the current class. Take the standard set of
|
13
|
+
# CouchRest::Database#view options.
|
14
|
+
def all(opts = {}, &block)
|
15
|
+
view(:all, opts, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the number of documents that have the "couchrest-type" field
|
19
|
+
# equal to the name of the current class. Takes the standard set of
|
20
|
+
# CouchRest::Database#view options
|
21
|
+
def count(opts = {}, &block)
|
22
|
+
all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
|
23
|
+
end
|
24
|
+
|
25
|
+
# Load the first document that have the "couchrest-type" field equal to
|
26
|
+
# the name of the current class.
|
27
|
+
#
|
28
|
+
# ==== Returns
|
29
|
+
# Object:: The first object instance available
|
30
|
+
# or
|
31
|
+
# Nil:: if no instances available
|
32
|
+
#
|
33
|
+
# ==== Parameters
|
34
|
+
# opts<Hash>::
|
35
|
+
# View options, see <tt>CouchRest::Database#view</tt> options for more info.
|
36
|
+
def first(opts = {})
|
37
|
+
first_instance = self.all(opts.merge!(:limit => 1))
|
38
|
+
first_instance.empty? ? nil : first_instance.first
|
39
|
+
end
|
40
|
+
|
41
|
+
# Load a document from the database by id
|
42
|
+
# No exceptions will be raised if the document isn't found
|
43
|
+
#
|
44
|
+
# ==== Returns
|
45
|
+
# Object:: if the document was found
|
46
|
+
# or
|
47
|
+
# Nil::
|
48
|
+
#
|
49
|
+
# === Parameters
|
50
|
+
# id<String, Integer>:: Document ID
|
51
|
+
# db<Database>:: optional option to pass a custom database to use
|
52
|
+
def get(id, db = database)
|
53
|
+
begin
|
54
|
+
get!(id, db)
|
55
|
+
rescue
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
alias :find :get
|
60
|
+
|
61
|
+
# Load a document from the database by id
|
62
|
+
# An exception will be raised if the document isn't found
|
63
|
+
#
|
64
|
+
# ==== Returns
|
65
|
+
# Object:: if the document was found
|
66
|
+
# or
|
67
|
+
# Exception
|
68
|
+
#
|
69
|
+
# === Parameters
|
70
|
+
# id<String, Integer>:: Document ID
|
71
|
+
# db<Database>:: optional option to pass a custom database to use
|
72
|
+
def get!(id, db = database)
|
73
|
+
doc = db.get id
|
74
|
+
create_from_database(doc)
|
75
|
+
end
|
76
|
+
alias :find! :get!
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Mixins
|
3
|
+
module ExtendedAttachments
|
4
|
+
|
5
|
+
# Add a file attachment to the current document. Expects
|
6
|
+
# :file and :name to be included in the arguments.
|
7
|
+
def create_attachment(args={})
|
8
|
+
raise ArgumentError unless args[:file] && args[:name]
|
9
|
+
return if has_attachment?(args[:name])
|
10
|
+
self['_attachments'] ||= {}
|
11
|
+
set_attachment_attr(args)
|
12
|
+
rescue ArgumentError => e
|
13
|
+
raise ArgumentError, 'You must specify :file and :name'
|
14
|
+
end
|
15
|
+
|
16
|
+
# reads the data from an attachment
|
17
|
+
def read_attachment(attachment_name)
|
18
|
+
database.fetch_attachment(self, attachment_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
# modifies a file attachment on the current doc
|
22
|
+
def update_attachment(args={})
|
23
|
+
raise ArgumentError unless args[:file] && args[:name]
|
24
|
+
return unless has_attachment?(args[:name])
|
25
|
+
delete_attachment(args[:name])
|
26
|
+
set_attachment_attr(args)
|
27
|
+
rescue ArgumentError => e
|
28
|
+
raise ArgumentError, 'You must specify :file and :name'
|
29
|
+
end
|
30
|
+
|
31
|
+
# deletes a file attachment from the current doc
|
32
|
+
def delete_attachment(attachment_name)
|
33
|
+
return unless self['_attachments']
|
34
|
+
self['_attachments'].delete attachment_name
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns true if attachment_name exists
|
38
|
+
def has_attachment?(attachment_name)
|
39
|
+
!!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
|
40
|
+
end
|
41
|
+
|
42
|
+
# returns URL to fetch the attachment from
|
43
|
+
def attachment_url(attachment_name)
|
44
|
+
return unless has_attachment?(attachment_name)
|
45
|
+
"#{database.root}/#{self.id}/#{attachment_name}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# returns URI to fetch the attachment from
|
49
|
+
def attachment_uri(attachment_name)
|
50
|
+
return unless has_attachment?(attachment_name)
|
51
|
+
"#{database.uri}/#{self.id}/#{attachment_name}"
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def get_mime_type(path)
|
57
|
+
return nil if path.nil?
|
58
|
+
type = ::MIME::Types.type_for(path)
|
59
|
+
type.empty? ? nil : type.first.content_type
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_attachment_attr(args)
|
63
|
+
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
|
64
|
+
content_type ||= (get_mime_type(args[:name]) || 'text/plain')
|
65
|
+
self['_attachments'][args[:name]] = {
|
66
|
+
'content_type' => content_type,
|
67
|
+
'data' => args[:file].read
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
end # module ExtendedAttachments
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'time'
|
2
|
+
require File.join(File.dirname(__FILE__), '..', 'property')
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'casted_array')
|
4
|
+
require File.join(File.dirname(__FILE__), '..', 'typecast')
|
5
|
+
|
6
|
+
module CouchRest
|
7
|
+
module Mixins
|
8
|
+
module Properties
|
9
|
+
|
10
|
+
class IncludeError < StandardError; end
|
11
|
+
|
12
|
+
include ::CouchRest::More::Typecast
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
base.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
16
|
+
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
|
17
|
+
self.properties ||= []
|
18
|
+
EOS
|
19
|
+
base.extend(ClassMethods)
|
20
|
+
raise CouchRest::Mixins::Properties::IncludeError, "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (base.new.respond_to?(:[]) && base.new.respond_to?(:[]=))
|
21
|
+
end
|
22
|
+
|
23
|
+
def apply_defaults
|
24
|
+
return if self.respond_to?(:new?) && (new? == false)
|
25
|
+
return unless self.class.respond_to?(:properties)
|
26
|
+
return if self.class.properties.empty?
|
27
|
+
# TODO: cache the default object
|
28
|
+
self.class.properties.each do |property|
|
29
|
+
key = property.name.to_s
|
30
|
+
# let's make sure we have a default
|
31
|
+
unless property.default.nil?
|
32
|
+
if property.default.class == Proc
|
33
|
+
self[key] = property.default.call
|
34
|
+
else
|
35
|
+
self[key] = Marshal.load(Marshal.dump(property.default))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def cast_keys
|
42
|
+
return unless self.class.properties
|
43
|
+
self.class.properties.each do |property|
|
44
|
+
cast_property(property)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def cast_property(property, assigned=false)
|
49
|
+
return unless property.casted
|
50
|
+
key = self.has_key?(property.name) ? property.name : property.name.to_sym
|
51
|
+
# Don't cast the property unless it has a value
|
52
|
+
return unless self[key]
|
53
|
+
if property.type.is_a?(Array)
|
54
|
+
klass = property.type[0]
|
55
|
+
self[key] = [self[key]] unless self[key].is_a?(Array)
|
56
|
+
arr = self[key].collect do |value|
|
57
|
+
value = typecast_value(value, klass, property.init_method)
|
58
|
+
associate_casted_to_parent(value, assigned)
|
59
|
+
value
|
60
|
+
end
|
61
|
+
# allow casted_by calls to be passed up chain by wrapping in CastedArray
|
62
|
+
self[key] = klass != String ? ::CouchRest::CastedArray.new(arr) : arr
|
63
|
+
self[key].casted_by = self if self[key].respond_to?(:casted_by)
|
64
|
+
else
|
65
|
+
self[key] = typecast_value(self[key], property.type, property.init_method)
|
66
|
+
associate_casted_to_parent(self[key], assigned)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def associate_casted_to_parent(casted, assigned)
|
71
|
+
casted.casted_by = self if casted.respond_to?(:casted_by)
|
72
|
+
casted.document_saved = true if !assigned && casted.respond_to?(:document_saved)
|
73
|
+
end
|
74
|
+
|
75
|
+
def cast_property_by_name(property_name)
|
76
|
+
return unless self.class.properties
|
77
|
+
property = self.class.properties.detect{|property| property.name == property_name}
|
78
|
+
return unless property
|
79
|
+
cast_property(property, true)
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
module ClassMethods
|
84
|
+
|
85
|
+
def property(name, *options)
|
86
|
+
opts = { }
|
87
|
+
type = options.shift
|
88
|
+
if type.class != Hash
|
89
|
+
opts[:type] = type
|
90
|
+
opts.merge!(options.shift || {})
|
91
|
+
else
|
92
|
+
opts.update(type)
|
93
|
+
end
|
94
|
+
existing_property = self.properties.find{|p| p.name == name.to_s}
|
95
|
+
if existing_property.nil? || (existing_property.default != opts[:default])
|
96
|
+
define_property(name, opts)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
# This is not a thread safe operation, if you have to set new properties at runtime
|
103
|
+
# make sure to use a mutex.
|
104
|
+
def define_property(name, options={})
|
105
|
+
# check if this property is going to casted
|
106
|
+
options[:casted] = !!(options[:cast_as] || options[:type])
|
107
|
+
property = CouchRest::Property.new(name, (options.delete(:cast_as) || options.delete(:type)), options)
|
108
|
+
create_property_getter(property)
|
109
|
+
create_property_setter(property) unless property.read_only == true
|
110
|
+
properties << property
|
111
|
+
end
|
112
|
+
|
113
|
+
# defines the getter for the property (and optional aliases)
|
114
|
+
def create_property_getter(property)
|
115
|
+
# meth = property.name
|
116
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
117
|
+
def #{property.name}
|
118
|
+
self['#{property.name}']
|
119
|
+
end
|
120
|
+
EOS
|
121
|
+
|
122
|
+
if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
|
123
|
+
class_eval <<-EOS, __FILE__, __LINE__
|
124
|
+
def #{property.name}?
|
125
|
+
if self['#{property.name}'].nil? || self['#{property.name}'] == false
|
126
|
+
false
|
127
|
+
else
|
128
|
+
true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
EOS
|
132
|
+
end
|
133
|
+
|
134
|
+
if property.alias
|
135
|
+
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
136
|
+
alias #{property.alias.to_sym} #{property.name.to_sym}
|
137
|
+
EOS
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# defines the setter for the property (and optional aliases)
|
142
|
+
def create_property_setter(property)
|
143
|
+
property_name = property.name
|
144
|
+
class_eval <<-EOS
|
145
|
+
def #{property_name}=(value)
|
146
|
+
self['#{property_name}'] = value
|
147
|
+
cast_property_by_name('#{property_name}')
|
148
|
+
end
|
149
|
+
EOS
|
150
|
+
|
151
|
+
if property.alias
|
152
|
+
class_eval <<-EOS
|
153
|
+
alias #{property.alias.to_sym}= #{property_name.to_sym}=
|
154
|
+
EOS
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end # module ClassMethods
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|