samlown-couchrest 0.35
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 +46 -0
- data/Rakefile +67 -0
- data/THANKS.md +19 -0
- data/examples/model/example.rb +144 -0
- data/examples/word_count/markov +38 -0
- data/examples/word_count/views/books/chunked-map.js +3 -0
- data/examples/word_count/views/books/united-map.js +1 -0
- data/examples/word_count/views/markov/chain-map.js +6 -0
- data/examples/word_count/views/markov/chain-reduce.js +7 -0
- data/examples/word_count/views/word_count/count-map.js +6 -0
- data/examples/word_count/views/word_count/count-reduce.js +3 -0
- data/examples/word_count/word_count.rb +46 -0
- data/examples/word_count/word_count_query.rb +40 -0
- data/examples/word_count/word_count_views.rb +26 -0
- data/history.txt +114 -0
- data/lib/couchrest/commands/generate.rb +71 -0
- data/lib/couchrest/commands/push.rb +103 -0
- data/lib/couchrest/core/adapters/restclient.rb +35 -0
- data/lib/couchrest/core/database.rb +377 -0
- data/lib/couchrest/core/design.rb +79 -0
- data/lib/couchrest/core/document.rb +84 -0
- data/lib/couchrest/core/http_abstraction.rb +48 -0
- data/lib/couchrest/core/response.rb +16 -0
- data/lib/couchrest/core/rest_api.rb +49 -0
- data/lib/couchrest/core/server.rb +88 -0
- data/lib/couchrest/core/view.rb +4 -0
- data/lib/couchrest/helper/pager.rb +103 -0
- data/lib/couchrest/helper/streamer.rb +51 -0
- data/lib/couchrest/helper/upgrade.rb +51 -0
- data/lib/couchrest/middlewares/logger.rb +263 -0
- data/lib/couchrest/mixins/attachments.rb +31 -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 +124 -0
- data/lib/couchrest/mixins/collection.rb +260 -0
- data/lib/couchrest/mixins/design_doc.rb +103 -0
- data/lib/couchrest/mixins/document_queries.rb +80 -0
- data/lib/couchrest/mixins/extended_attachments.rb +70 -0
- data/lib/couchrest/mixins/extended_document_mixins.rb +9 -0
- data/lib/couchrest/mixins/properties.rb +154 -0
- data/lib/couchrest/mixins/validation.rb +246 -0
- data/lib/couchrest/mixins/views.rb +173 -0
- data/lib/couchrest/mixins.rb +4 -0
- data/lib/couchrest/monkeypatches.rb +113 -0
- data/lib/couchrest/more/casted_model.rb +58 -0
- data/lib/couchrest/more/extended_document.rb +310 -0
- data/lib/couchrest/more/property.rb +50 -0
- data/lib/couchrest/more/typecast.rb +175 -0
- data/lib/couchrest/support/blank.rb +42 -0
- data/lib/couchrest/support/class.rb +190 -0
- data/lib/couchrest/support/rails.rb +42 -0
- data/lib/couchrest/validation/auto_validate.rb +157 -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.rb +162 -0
- data/spec/couchrest/core/couchrest_spec.rb +184 -0
- data/spec/couchrest/core/database_spec.rb +840 -0
- data/spec/couchrest/core/design_spec.rb +138 -0
- data/spec/couchrest/core/document_spec.rb +275 -0
- data/spec/couchrest/core/server_spec.rb +35 -0
- data/spec/couchrest/helpers/pager_spec.rb +122 -0
- data/spec/couchrest/helpers/streamer_spec.rb +52 -0
- data/spec/couchrest/more/attribute_protection_spec.rb +150 -0
- data/spec/couchrest/more/casted_extended_doc_spec.rb +79 -0
- data/spec/couchrest/more/casted_model_spec.rb +406 -0
- data/spec/couchrest/more/extended_doc_attachment_spec.rb +135 -0
- data/spec/couchrest/more/extended_doc_inherited_spec.rb +40 -0
- data/spec/couchrest/more/extended_doc_spec.rb +797 -0
- data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
- data/spec/couchrest/more/extended_doc_view_spec.rb +456 -0
- data/spec/couchrest/more/property_spec.rb +628 -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 +20 -0
- data/spec/fixtures/more/course.rb +22 -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 +6 -0
- data/spec/spec_helper.rb +49 -0
- data/utils/remap.rb +27 -0
- data/utils/subset.rb +30 -0
- metadata +223 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
require 'mime/types'
|
|
2
|
+
require File.join(File.dirname(__FILE__), "property")
|
|
3
|
+
require File.join(File.dirname(__FILE__), '..', 'mixins', 'extended_document_mixins')
|
|
4
|
+
require "enumerator"
|
|
5
|
+
|
|
6
|
+
module CouchRest
|
|
7
|
+
|
|
8
|
+
# Same as CouchRest::Document but with properties and validations
|
|
9
|
+
class ExtendedDocument < Document
|
|
10
|
+
include CouchRest::Callbacks
|
|
11
|
+
include CouchRest::Mixins::DocumentQueries
|
|
12
|
+
include CouchRest::Mixins::Views
|
|
13
|
+
include CouchRest::Mixins::DesignDoc
|
|
14
|
+
include CouchRest::Mixins::ExtendedAttachments
|
|
15
|
+
include CouchRest::Mixins::ClassProxy
|
|
16
|
+
include CouchRest::Mixins::Collection
|
|
17
|
+
include CouchRest::Mixins::AttributeProtection
|
|
18
|
+
|
|
19
|
+
def self.subclasses
|
|
20
|
+
@subclasses ||= []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.inherited(subklass)
|
|
24
|
+
super
|
|
25
|
+
subklass.send(:include, CouchRest::Mixins::Properties)
|
|
26
|
+
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
|
27
|
+
def self.inherited(subklass)
|
|
28
|
+
super
|
|
29
|
+
subklass.properties = self.properties.dup
|
|
30
|
+
end
|
|
31
|
+
EOS
|
|
32
|
+
subclasses << subklass
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Accessors
|
|
36
|
+
attr_accessor :casted_by
|
|
37
|
+
|
|
38
|
+
# Callbacks
|
|
39
|
+
define_callbacks :create, "result == :halt"
|
|
40
|
+
define_callbacks :save, "result == :halt"
|
|
41
|
+
define_callbacks :update, "result == :halt"
|
|
42
|
+
define_callbacks :destroy, "result == :halt"
|
|
43
|
+
|
|
44
|
+
# Creates a new instance, bypassing attribute protection
|
|
45
|
+
#
|
|
46
|
+
# ==== Returns
|
|
47
|
+
# a document instance
|
|
48
|
+
def self.create_from_database(passed_keys={})
|
|
49
|
+
new(passed_keys, :directly_set_attributes => true)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize(passed_keys={}, options={})
|
|
53
|
+
apply_defaults # defined in CouchRest::Mixins::Properties
|
|
54
|
+
remove_protected_attributes(passed_keys) unless options[:directly_set_attributes]
|
|
55
|
+
directly_set_attributes(passed_keys) unless passed_keys.nil?
|
|
56
|
+
super(passed_keys)
|
|
57
|
+
cast_keys # defined in CouchRest::Mixins::Properties
|
|
58
|
+
unless self['_id'] && self['_rev']
|
|
59
|
+
self['couchrest-type'] = self.class.to_s
|
|
60
|
+
end
|
|
61
|
+
after_initialize if respond_to?(:after_initialize)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Defines an instance and save it directly to the database
|
|
65
|
+
#
|
|
66
|
+
# ==== Returns
|
|
67
|
+
# returns the reloaded document
|
|
68
|
+
def self.create(options)
|
|
69
|
+
instance = new(options)
|
|
70
|
+
instance.create
|
|
71
|
+
instance
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Defines an instance and save it directly to the database
|
|
75
|
+
#
|
|
76
|
+
# ==== Returns
|
|
77
|
+
# returns the reloaded document or raises an exception
|
|
78
|
+
def self.create!(options)
|
|
79
|
+
instance = new(options)
|
|
80
|
+
instance.create!
|
|
81
|
+
instance
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
|
|
85
|
+
# on the document whenever saving occurs. CouchRest uses a pretty
|
|
86
|
+
# decent time format by default. See Time#to_json
|
|
87
|
+
def self.timestamps!
|
|
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)
|
|
91
|
+
|
|
92
|
+
set_callback :save, :before do |object|
|
|
93
|
+
object['updated_at'] = Time.now
|
|
94
|
+
object['created_at'] = object['updated_at'] if object.new?
|
|
95
|
+
end
|
|
96
|
+
EOS
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Name a method that will be called before the document is first saved,
|
|
100
|
+
# which returns a string to be used for the document's <tt>_id</tt>.
|
|
101
|
+
# Because CouchDB enforces a constraint that each id must be unique,
|
|
102
|
+
# this can be used to enforce eg: uniq usernames. Note that this id
|
|
103
|
+
# must be globally unique across all document types which share a
|
|
104
|
+
# database, so if you'd like to scope uniqueness to this class, you
|
|
105
|
+
# should use the class name as part of the unique id.
|
|
106
|
+
def self.unique_id method = nil, &block
|
|
107
|
+
if method
|
|
108
|
+
define_method :set_unique_id do
|
|
109
|
+
self['_id'] ||= self.send(method)
|
|
110
|
+
end
|
|
111
|
+
elsif block
|
|
112
|
+
define_method :set_unique_id do
|
|
113
|
+
uniqid = block.call(self)
|
|
114
|
+
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
|
|
115
|
+
self['_id'] ||= uniqid
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Temp solution to make the view_by methods available
|
|
121
|
+
def self.method_missing(m, *args, &block)
|
|
122
|
+
if has_view?(m)
|
|
123
|
+
query = args.shift || {}
|
|
124
|
+
view(m, query, *args, &block)
|
|
125
|
+
else
|
|
126
|
+
super
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
### instance methods
|
|
131
|
+
|
|
132
|
+
# Returns the Class properties
|
|
133
|
+
#
|
|
134
|
+
# ==== Returns
|
|
135
|
+
# Array:: the list of properties for the instance
|
|
136
|
+
def properties
|
|
137
|
+
self.class.properties
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Gets a reference to the actual document in the DB
|
|
141
|
+
# Calls up to the next document if there is one,
|
|
142
|
+
# Otherwise we're at the top and we return self
|
|
143
|
+
def base_doc
|
|
144
|
+
return self if base_doc?
|
|
145
|
+
@casted_by.base_doc
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Checks if we're the top document
|
|
149
|
+
def base_doc?
|
|
150
|
+
!@casted_by
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Takes a hash as argument, and applies the values by using writer methods
|
|
154
|
+
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
|
|
155
|
+
# missing. In case of error, no attributes are changed.
|
|
156
|
+
def update_attributes_without_saving(hash)
|
|
157
|
+
# remove attributes that cannot be updated, silently ignoring them
|
|
158
|
+
# which matches Rails behavior when, for instance, setting created_at.
|
|
159
|
+
# make a copy, we don't want to change arguments
|
|
160
|
+
attrs = hash.dup
|
|
161
|
+
%w[_id _rev created_at updated_at].each {|attr| attrs.delete(attr)}
|
|
162
|
+
check_properties_exist(attrs)
|
|
163
|
+
set_attributes(attrs)
|
|
164
|
+
end
|
|
165
|
+
alias :attributes= :update_attributes_without_saving
|
|
166
|
+
|
|
167
|
+
# Takes a hash as argument, and applies the values by using writer methods
|
|
168
|
+
# for each key. Raises a NoMethodError if the corresponding methods are
|
|
169
|
+
# missing. In case of error, no attributes are changed.
|
|
170
|
+
def update_attributes(hash)
|
|
171
|
+
update_attributes_without_saving hash
|
|
172
|
+
save
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# for compatibility with old-school frameworks
|
|
176
|
+
alias :new_record? :new?
|
|
177
|
+
alias :new_document? :new?
|
|
178
|
+
|
|
179
|
+
# Trigger the callbacks (before, after, around)
|
|
180
|
+
# and create the document
|
|
181
|
+
# It's important to have a create callback since you can't check if a document
|
|
182
|
+
# was new after you saved it
|
|
183
|
+
#
|
|
184
|
+
# When creating a document, both the create and the save callbacks will be triggered.
|
|
185
|
+
def create(bulk = false)
|
|
186
|
+
caught = catch(:halt) do
|
|
187
|
+
_run_create_callbacks do
|
|
188
|
+
_run_save_callbacks do
|
|
189
|
+
create_without_callbacks(bulk)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# unlike save, create returns the newly created document
|
|
196
|
+
def create_without_callbacks(bulk =false)
|
|
197
|
+
raise ArgumentError, "a document requires a database to be created to (The document or the #{self.class} default database were not set)" unless database
|
|
198
|
+
set_unique_id if new? && self.respond_to?(:set_unique_id)
|
|
199
|
+
result = database.save_doc(self, bulk)
|
|
200
|
+
(result["ok"] == true) ? self : false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Creates the document in the db. Raises an exception
|
|
204
|
+
# if the document is not created properly.
|
|
205
|
+
def create!
|
|
206
|
+
raise "#{self.inspect} failed to save" unless self.create
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Trigger the callbacks (before, after, around)
|
|
210
|
+
# only if the document isn't new
|
|
211
|
+
def update(bulk = false)
|
|
212
|
+
caught = catch(:halt) do
|
|
213
|
+
if self.new?
|
|
214
|
+
save(bulk)
|
|
215
|
+
else
|
|
216
|
+
_run_update_callbacks do
|
|
217
|
+
_run_save_callbacks do
|
|
218
|
+
save_without_callbacks(bulk)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Trigger the callbacks (before, after, around)
|
|
226
|
+
# and save the document
|
|
227
|
+
def save(bulk = false)
|
|
228
|
+
caught = catch(:halt) do
|
|
229
|
+
if self.new?
|
|
230
|
+
_run_save_callbacks do
|
|
231
|
+
save_without_callbacks(bulk)
|
|
232
|
+
end
|
|
233
|
+
else
|
|
234
|
+
update(bulk)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Overridden to set the unique ID.
|
|
240
|
+
# Returns a boolean value
|
|
241
|
+
def save_without_callbacks(bulk = false)
|
|
242
|
+
raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database
|
|
243
|
+
set_unique_id if new? && self.respond_to?(:set_unique_id)
|
|
244
|
+
result = database.save_doc(self, bulk)
|
|
245
|
+
mark_as_saved
|
|
246
|
+
result["ok"] == true
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Saves the document to the db using save. Raises an exception
|
|
250
|
+
# if the document is not saved properly.
|
|
251
|
+
def save!
|
|
252
|
+
raise "#{self.inspect} failed to save" unless self.save
|
|
253
|
+
true
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Deletes the document from the database. Runs the :destroy callbacks.
|
|
257
|
+
# Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
|
|
258
|
+
# document to be saved to a new <tt>_id</tt>.
|
|
259
|
+
def destroy(bulk=false)
|
|
260
|
+
caught = catch(:halt) do
|
|
261
|
+
_run_destroy_callbacks do
|
|
262
|
+
result = database.delete_doc(self, bulk)
|
|
263
|
+
if result['ok']
|
|
264
|
+
self.delete('_rev')
|
|
265
|
+
self.delete('_id')
|
|
266
|
+
end
|
|
267
|
+
result['ok']
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
protected
|
|
273
|
+
|
|
274
|
+
# Set document_saved flag on all casted models to true
|
|
275
|
+
def mark_as_saved
|
|
276
|
+
self.each do |key, prop|
|
|
277
|
+
if prop.is_a?(Array)
|
|
278
|
+
prop.each do |item|
|
|
279
|
+
if item.respond_to?(:document_saved)
|
|
280
|
+
item.send(:document_saved=, true)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
elsif prop.respond_to?(:document_saved)
|
|
284
|
+
prop.send(:document_saved=, true)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
def check_properties_exist(attrs)
|
|
292
|
+
attrs.each do |attribute_name, attribute_value|
|
|
293
|
+
raise NoMethodError, "#{attribute_name}= method not available, use property :#{attribute_name}" unless self.respond_to?("#{attribute_name}=")
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def directly_set_attributes(hash)
|
|
298
|
+
hash.each do |attribute_name, attribute_value|
|
|
299
|
+
if self.respond_to?("#{attribute_name}=")
|
|
300
|
+
self.send("#{attribute_name}=", hash.delete(attribute_name))
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def set_attributes(hash)
|
|
306
|
+
attrs = remove_protected_attributes(hash)
|
|
307
|
+
directly_set_attributes(attrs)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module CouchRest
|
|
2
|
+
|
|
3
|
+
# Basic attribute support for adding getter/setter + validation
|
|
4
|
+
class Property
|
|
5
|
+
attr_reader :name, :type, :read_only, :alias, :default, :casted, :init_method, :options
|
|
6
|
+
|
|
7
|
+
# attribute to define
|
|
8
|
+
def initialize(name, type = nil, options = {})
|
|
9
|
+
@name = name.to_s
|
|
10
|
+
parse_type(type)
|
|
11
|
+
parse_options(options)
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def parse_type(type)
|
|
18
|
+
if type.nil?
|
|
19
|
+
@type = String
|
|
20
|
+
elsif type.is_a?(Array) && type.empty?
|
|
21
|
+
@type = [Object]
|
|
22
|
+
else
|
|
23
|
+
base_type = type.is_a?(Array) ? type.first : type
|
|
24
|
+
if base_type.is_a?(String)
|
|
25
|
+
if base_type.downcase == 'boolean'
|
|
26
|
+
base_type = TrueClass
|
|
27
|
+
else
|
|
28
|
+
begin
|
|
29
|
+
base_type = ::CouchRest.constantize(base_type)
|
|
30
|
+
rescue # leave base type as a string and convert in more/typecast
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
@type = type.is_a?(Array) ? [base_type] : base_type
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_options(options)
|
|
39
|
+
return if options.empty?
|
|
40
|
+
@validation_format = options.delete(:format) if options[:format]
|
|
41
|
+
@read_only = options.delete(:read_only) if options[:read_only]
|
|
42
|
+
@alias = options.delete(:alias) if options[:alias]
|
|
43
|
+
@default = options.delete(:default) unless options[:default].nil?
|
|
44
|
+
@casted = options[:casted] ? true : false
|
|
45
|
+
@init_method = options[:init_method] ? options.delete(:init_method) : 'new'
|
|
46
|
+
@options = options
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
require 'bigdecimal'
|
|
3
|
+
require 'bigdecimal/util'
|
|
4
|
+
require File.join(File.dirname(__FILE__), '..', 'more', 'property')
|
|
5
|
+
|
|
6
|
+
class Time
|
|
7
|
+
# returns a local time value much faster than Time.parse
|
|
8
|
+
def self.mktime_with_offset(string)
|
|
9
|
+
string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/
|
|
10
|
+
# $1 = year
|
|
11
|
+
# $2 = month
|
|
12
|
+
# $3 = day
|
|
13
|
+
# $4 = hours
|
|
14
|
+
# $5 = minutes
|
|
15
|
+
# $6 = seconds
|
|
16
|
+
# $7 = time zone direction
|
|
17
|
+
# $8 = tz difference
|
|
18
|
+
# utc time with wrong TZ info:
|
|
19
|
+
time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7)
|
|
20
|
+
tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600)
|
|
21
|
+
time + tz_difference + zone_offset(time.zone)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module CouchRest
|
|
26
|
+
module More
|
|
27
|
+
module Typecast
|
|
28
|
+
|
|
29
|
+
def typecast_value(value, klass, init_method)
|
|
30
|
+
return nil if value.nil?
|
|
31
|
+
klass = ::CouchRest.constantize(klass) unless klass.is_a?(Class)
|
|
32
|
+
if value.instance_of?(klass) || klass == Object
|
|
33
|
+
value
|
|
34
|
+
elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
|
|
35
|
+
send('typecast_to_'+klass.to_s.downcase, value)
|
|
36
|
+
else
|
|
37
|
+
# Allow the init_method to be defined as a Proc for advanced conversion
|
|
38
|
+
init_method.is_a?(Proc) ? init_method.call(value) : klass.send(init_method, value)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
protected
|
|
43
|
+
|
|
44
|
+
# Typecast a value to an Integer
|
|
45
|
+
def typecast_to_integer(value)
|
|
46
|
+
typecast_to_numeric(value, :to_i)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Typecast a value to a String
|
|
50
|
+
def typecast_to_string(value)
|
|
51
|
+
value.to_s
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Typecast a value to a true or false
|
|
55
|
+
def typecast_to_trueclass(value)
|
|
56
|
+
if value.kind_of?(Integer)
|
|
57
|
+
return true if value == 1
|
|
58
|
+
return false if value == 0
|
|
59
|
+
elsif value.respond_to?(:to_s)
|
|
60
|
+
return true if %w[ true 1 t ].include?(value.to_s.downcase)
|
|
61
|
+
return false if %w[ false 0 f ].include?(value.to_s.downcase)
|
|
62
|
+
end
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Typecast a value to a BigDecimal
|
|
67
|
+
def typecast_to_bigdecimal(value)
|
|
68
|
+
if value.kind_of?(Integer)
|
|
69
|
+
value.to_s.to_d
|
|
70
|
+
else
|
|
71
|
+
typecast_to_numeric(value, :to_d)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Typecast a value to a Float
|
|
76
|
+
def typecast_to_float(value)
|
|
77
|
+
typecast_to_numeric(value, :to_f)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Match numeric string
|
|
81
|
+
def typecast_to_numeric(value, method)
|
|
82
|
+
if value.respond_to?(:to_str)
|
|
83
|
+
if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
|
|
84
|
+
$1.send(method)
|
|
85
|
+
else
|
|
86
|
+
value
|
|
87
|
+
end
|
|
88
|
+
elsif value.respond_to?(method)
|
|
89
|
+
value.send(method)
|
|
90
|
+
else
|
|
91
|
+
value
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Typecasts an arbitrary value to a DateTime.
|
|
96
|
+
# Handles both Hashes and DateTime instances.
|
|
97
|
+
# This is slow!! Use Time instead.
|
|
98
|
+
def typecast_to_datetime(value)
|
|
99
|
+
if value.is_a?(Hash)
|
|
100
|
+
typecast_hash_to_datetime(value)
|
|
101
|
+
else
|
|
102
|
+
DateTime.parse(value.to_s)
|
|
103
|
+
end
|
|
104
|
+
rescue ArgumentError
|
|
105
|
+
value
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Typecasts an arbitrary value to a Date
|
|
109
|
+
# Handles both Hashes and Date instances.
|
|
110
|
+
def typecast_to_date(value)
|
|
111
|
+
if value.is_a?(Hash)
|
|
112
|
+
typecast_hash_to_date(value)
|
|
113
|
+
elsif value.is_a?(Time) # sometimes people think date is time!
|
|
114
|
+
value.to_date
|
|
115
|
+
elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
|
|
116
|
+
# Faster than parsing the date
|
|
117
|
+
Date.new($1.to_i, $2.to_i, $3.to_i)
|
|
118
|
+
else
|
|
119
|
+
Date.parse(value)
|
|
120
|
+
end
|
|
121
|
+
rescue ArgumentError
|
|
122
|
+
value
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Typecasts an arbitrary value to a Time
|
|
126
|
+
# Handles both Hashes and Time instances.
|
|
127
|
+
def typecast_to_time(value)
|
|
128
|
+
if value.is_a?(Hash)
|
|
129
|
+
typecast_hash_to_time(value)
|
|
130
|
+
else
|
|
131
|
+
Time.mktime_with_offset(value.to_s)
|
|
132
|
+
end
|
|
133
|
+
rescue ArgumentError
|
|
134
|
+
value
|
|
135
|
+
rescue TypeError
|
|
136
|
+
value
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Creates a DateTime instance from a Hash with keys :year, :month, :day,
|
|
140
|
+
# :hour, :min, :sec
|
|
141
|
+
def typecast_hash_to_datetime(value)
|
|
142
|
+
DateTime.new(*extract_time(value))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Creates a Date instance from a Hash with keys :year, :month, :day
|
|
146
|
+
def typecast_hash_to_date(value)
|
|
147
|
+
Date.new(*extract_time(value)[0, 3])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Creates a Time instance from a Hash with keys :year, :month, :day,
|
|
151
|
+
# :hour, :min, :sec
|
|
152
|
+
def typecast_hash_to_time(value)
|
|
153
|
+
Time.local(*extract_time(value))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Extracts the given args from the hash. If a value does not exist, it
|
|
157
|
+
# uses the value of Time.now.
|
|
158
|
+
def extract_time(value)
|
|
159
|
+
now = Time.now
|
|
160
|
+
[:year, :month, :day, :hour, :min, :sec].map do |segment|
|
|
161
|
+
typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Typecast a value to a Class
|
|
166
|
+
def typecast_to_class(value)
|
|
167
|
+
::CouchRest.constantize(value.to_s)
|
|
168
|
+
rescue NameError
|
|
169
|
+
value
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# blank? methods for several different class types
|
|
2
|
+
class Object
|
|
3
|
+
# Returns true if the object is nil or empty (if applicable)
|
|
4
|
+
def blank?
|
|
5
|
+
nil? || (respond_to?(:empty?) && empty?)
|
|
6
|
+
end
|
|
7
|
+
end # class Object
|
|
8
|
+
|
|
9
|
+
class Numeric
|
|
10
|
+
# Numerics can't be blank
|
|
11
|
+
def blank?
|
|
12
|
+
false
|
|
13
|
+
end
|
|
14
|
+
end # class Numeric
|
|
15
|
+
|
|
16
|
+
class NilClass
|
|
17
|
+
# Nils are always blank
|
|
18
|
+
def blank?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
end # class NilClass
|
|
22
|
+
|
|
23
|
+
class TrueClass
|
|
24
|
+
# True is not blank.
|
|
25
|
+
def blank?
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
end # class TrueClass
|
|
29
|
+
|
|
30
|
+
class FalseClass
|
|
31
|
+
# False is always blank.
|
|
32
|
+
def blank?
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
end # class FalseClass
|
|
36
|
+
|
|
37
|
+
class String
|
|
38
|
+
# Strips out whitespace then tests if the string is empty.
|
|
39
|
+
def blank?
|
|
40
|
+
strip.empty?
|
|
41
|
+
end
|
|
42
|
+
end # class String
|