couchrest 0.12.4 → 0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/README.md +33 -8
  2. data/Rakefile +11 -2
  3. data/examples/model/example.rb +19 -13
  4. data/lib/couchrest.rb +70 -11
  5. data/lib/couchrest/core/database.rb +121 -62
  6. data/lib/couchrest/core/design.rb +7 -17
  7. data/lib/couchrest/core/document.rb +42 -30
  8. data/lib/couchrest/core/response.rb +16 -0
  9. data/lib/couchrest/core/server.rb +47 -10
  10. data/lib/couchrest/helper/upgrade.rb +51 -0
  11. data/lib/couchrest/mixins.rb +4 -0
  12. data/lib/couchrest/mixins/attachments.rb +31 -0
  13. data/lib/couchrest/mixins/callbacks.rb +483 -0
  14. data/lib/couchrest/mixins/class_proxy.rb +108 -0
  15. data/lib/couchrest/mixins/design_doc.rb +90 -0
  16. data/lib/couchrest/mixins/document_queries.rb +44 -0
  17. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  18. data/lib/couchrest/mixins/extended_document_mixins.rb +7 -0
  19. data/lib/couchrest/mixins/properties.rb +129 -0
  20. data/lib/couchrest/mixins/validation.rb +242 -0
  21. data/lib/couchrest/mixins/views.rb +169 -0
  22. data/lib/couchrest/monkeypatches.rb +81 -6
  23. data/lib/couchrest/more/casted_model.rb +28 -0
  24. data/lib/couchrest/more/extended_document.rb +215 -0
  25. data/lib/couchrest/more/property.rb +40 -0
  26. data/lib/couchrest/support/blank.rb +42 -0
  27. data/lib/couchrest/support/class.rb +176 -0
  28. data/lib/couchrest/validation/auto_validate.rb +163 -0
  29. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  30. data/lib/couchrest/validation/validation_errors.rb +118 -0
  31. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  32. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  33. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  34. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  35. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  36. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  37. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  38. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  39. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  40. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  41. data/spec/couchrest/core/database_spec.rb +189 -124
  42. data/spec/couchrest/core/design_spec.rb +13 -6
  43. data/spec/couchrest/core/document_spec.rb +231 -177
  44. data/spec/couchrest/core/server_spec.rb +35 -0
  45. data/spec/couchrest/helpers/pager_spec.rb +1 -1
  46. data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
  47. data/spec/couchrest/more/casted_model_spec.rb +98 -0
  48. data/spec/couchrest/more/extended_doc_attachment_spec.rb +130 -0
  49. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  50. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  51. data/spec/couchrest/more/extended_doc_view_spec.rb +355 -0
  52. data/spec/couchrest/more/property_spec.rb +136 -0
  53. data/spec/fixtures/more/article.rb +34 -0
  54. data/spec/fixtures/more/card.rb +20 -0
  55. data/spec/fixtures/more/course.rb +14 -0
  56. data/spec/fixtures/more/event.rb +6 -0
  57. data/spec/fixtures/more/invoice.rb +17 -0
  58. data/spec/fixtures/more/person.rb +8 -0
  59. data/spec/fixtures/more/question.rb +6 -0
  60. data/spec/fixtures/more/service.rb +12 -0
  61. data/spec/spec_helper.rb +13 -7
  62. metadata +58 -4
  63. data/lib/couchrest/core/model.rb +0 -613
  64. data/spec/couchrest/core/model_spec.rb +0 -855
@@ -0,0 +1,108 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module ClassProxy
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # Return a proxy object which represents a model class on a
12
+ # chosen database instance. This allows you to DRY operations
13
+ # where a database is chosen dynamically.
14
+ #
15
+ # ==== Example:
16
+ #
17
+ # db = CouchRest::Database.new(...)
18
+ # articles = Article.on(db)
19
+ #
20
+ # articles.all { ... }
21
+ # articles.by_title { ... }
22
+ #
23
+ # u = articles.get("someid")
24
+ #
25
+ # u = articles.new(:title => "I like plankton")
26
+ # u.save # saved on the correct database
27
+
28
+ def on(database)
29
+ Proxy.new(self, database)
30
+ end
31
+ end
32
+
33
+ class Proxy #:nodoc:
34
+ def initialize(klass, database)
35
+ @klass = klass
36
+ @database = database
37
+ end
38
+
39
+ # ExtendedDocument
40
+
41
+ def new(*args)
42
+ doc = @klass.new(*args)
43
+ doc.database = @database
44
+ doc
45
+ end
46
+
47
+ def method_missing(m, *args, &block)
48
+ if has_view?(m)
49
+ query = args.shift || {}
50
+ view(m, query, *args, &block)
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ # Mixins::DocumentQueries
57
+
58
+ def all(opts = {}, &block)
59
+ @klass.all({:database => @database}.merge(opts), &block)
60
+ end
61
+
62
+ def first(opts = {})
63
+ @klass.first({:database => @database}.merge(opts))
64
+ end
65
+
66
+ def get(id)
67
+ @klass.get(id, @database)
68
+ end
69
+
70
+ # Mixins::Views
71
+
72
+ def has_view?(view)
73
+ @klass.has_view?(view)
74
+ end
75
+
76
+ def view(name, query={}, &block)
77
+ @klass.view(name, {:database => @database}.merge(query), &block)
78
+ end
79
+
80
+ def all_design_doc_versions
81
+ @klass.all_design_doc_versions(@database)
82
+ end
83
+
84
+ def cleanup_design_docs!
85
+ @klass.cleanup_design_docs!(@database)
86
+ end
87
+
88
+ # Mixins::DesignDoc
89
+
90
+ def design_doc
91
+ @klass.design_doc
92
+ end
93
+
94
+ def design_doc_fresh
95
+ @klass.design_doc_fresh
96
+ end
97
+
98
+ def refresh_design_doc
99
+ @klass.refresh_design_doc
100
+ end
101
+
102
+ def save_design_doc
103
+ @klass.save_design_doc_on(@database)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,90 @@
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
+ attr_accessor :design_doc, :design_doc_slug_cache, :design_doc_fresh
13
+
14
+ def design_doc
15
+ @design_doc ||= Design.new(default_design_doc)
16
+ end
17
+
18
+ def design_doc_id
19
+ "_design/#{design_doc_slug}"
20
+ end
21
+
22
+ def design_doc_slug
23
+ return design_doc_slug_cache if (design_doc_slug_cache && design_doc_fresh)
24
+ funcs = []
25
+ design_doc['views'].each do |name, view|
26
+ funcs << "#{name}/#{view['map']}#{view['reduce']}"
27
+ end
28
+ md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
29
+ self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
30
+ end
31
+
32
+ def default_design_doc
33
+ {
34
+ "language" => "javascript",
35
+ "views" => {
36
+ 'all' => {
37
+ 'map' => "function(doc) {
38
+ if (doc['couchrest-type'] == '#{self.to_s}') {
39
+ emit(null,null);
40
+ }
41
+ }"
42
+ }
43
+ }
44
+ }
45
+ end
46
+
47
+ def refresh_design_doc
48
+ design_doc['_id'] = design_doc_id
49
+ design_doc.delete('_rev')
50
+ #design_doc.database = nil
51
+ self.design_doc_fresh = true
52
+ end
53
+
54
+ # Save the design doc onto the default database, and update the
55
+ # design_doc attribute
56
+ def save_design_doc
57
+ refresh_design_doc unless design_doc_fresh
58
+ self.design_doc = update_design_doc(design_doc)
59
+ end
60
+
61
+ # Save the design doc onto a target database in a thread-safe way,
62
+ # not modifying the model's design_doc
63
+ def save_design_doc_on(db)
64
+ update_design_doc(Design.new(design_doc), db)
65
+ end
66
+
67
+ private
68
+
69
+ # Writes out a design_doc to a given database, returning the
70
+ # updated design doc
71
+ def update_design_doc(design_doc, db = database)
72
+ saved = db.get(design_doc['_id']) rescue nil
73
+ if saved
74
+ design_doc['views'].each do |name, view|
75
+ saved['views'][name] = view
76
+ end
77
+ db.save_doc(saved)
78
+ saved
79
+ else
80
+ design_doc.database = db
81
+ design_doc.save
82
+ design_doc
83
+ end
84
+ end
85
+
86
+ end # module ClassMethods
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,44 @@
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
+ # Load the first document that have the "couchrest-type" field equal to
19
+ # the name of the current class.
20
+ #
21
+ # ==== Returns
22
+ # Object:: The first object instance available
23
+ # or
24
+ # Nil:: if no instances available
25
+ #
26
+ # ==== Parameters
27
+ # opts<Hash>::
28
+ # View options, see <tt>CouchRest::Database#view</tt> options for more info.
29
+ def first(opts = {})
30
+ first_instance = self.all(opts.merge!(:limit => 1))
31
+ first_instance.empty? ? nil : first_instance.first
32
+ end
33
+
34
+ # Load a document from the database by id
35
+ def get(id, db = database)
36
+ doc = db.get id
37
+ new(doc)
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module ExtendedAttachments
4
+
5
+ # creates a file attachment to the current doc
6
+ def create_attachment(args={})
7
+ raise ArgumentError unless args[:file] && args[:name]
8
+ return if has_attachment?(args[:name])
9
+ self['_attachments'] ||= {}
10
+ set_attachment_attr(args)
11
+ rescue ArgumentError => e
12
+ raise ArgumentError, 'You must specify :file and :name'
13
+ end
14
+
15
+ # reads the data from an attachment
16
+ def read_attachment(attachment_name)
17
+ Base64.decode64(database.fetch_attachment(self, attachment_name))
18
+ end
19
+
20
+ # modifies a file attachment on the current doc
21
+ def update_attachment(args={})
22
+ raise ArgumentError unless args[:file] && args[:name]
23
+ return unless has_attachment?(args[:name])
24
+ delete_attachment(args[:name])
25
+ set_attachment_attr(args)
26
+ rescue ArgumentError => e
27
+ raise ArgumentError, 'You must specify :file and :name'
28
+ end
29
+
30
+ # deletes a file attachment from the current doc
31
+ def delete_attachment(attachment_name)
32
+ return unless self['_attachments']
33
+ self['_attachments'].delete attachment_name
34
+ end
35
+
36
+ # returns true if attachment_name exists
37
+ def has_attachment?(attachment_name)
38
+ !!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
39
+ end
40
+
41
+ # returns URL to fetch the attachment from
42
+ def attachment_url(attachment_name)
43
+ return unless has_attachment?(attachment_name)
44
+ "#{database.root}/#{self.id}/#{attachment_name}"
45
+ end
46
+
47
+ private
48
+
49
+ def encode_attachment(data)
50
+ ::Base64.encode64(data).gsub(/\r|\n/,'')
51
+ end
52
+
53
+ def get_mime_type(file)
54
+ ::MIME::Types.type_for(file.path).empty? ?
55
+ 'text\/plain' : MIME::Types.type_for(file.path).first.content_type.gsub(/\//,'\/')
56
+ end
57
+
58
+ def set_attachment_attr(args)
59
+ content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file])
60
+ self['_attachments'][args[:name]] = {
61
+ 'content-type' => content_type,
62
+ 'data' => encode_attachment(args[:file].read)
63
+ }
64
+ end
65
+
66
+ end # module ExtendedAttachments
67
+ end
68
+ end
@@ -0,0 +1,7 @@
1
+ require File.join(File.dirname(__FILE__), 'properties')
2
+ require File.join(File.dirname(__FILE__), 'document_queries')
3
+ require File.join(File.dirname(__FILE__), 'views')
4
+ require File.join(File.dirname(__FILE__), 'design_doc')
5
+ require File.join(File.dirname(__FILE__), 'validation')
6
+ require File.join(File.dirname(__FILE__), 'extended_attachments')
7
+ require File.join(File.dirname(__FILE__), 'class_proxy')
@@ -0,0 +1,129 @@
1
+ require 'time'
2
+ require File.join(File.dirname(__FILE__), '..', 'more', 'property')
3
+
4
+ module CouchRest
5
+ module Mixins
6
+ module Properties
7
+
8
+ class IncludeError < StandardError; end
9
+
10
+ def self.included(base)
11
+ base.class_eval <<-EOS, __FILE__, __LINE__
12
+ extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
13
+ self.properties ||= []
14
+ EOS
15
+ base.extend(ClassMethods)
16
+ 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?(:[]=))
17
+ end
18
+
19
+ def apply_defaults
20
+ return unless self.respond_to?(:new_document?) && new_document?
21
+ return unless self.class.respond_to?(:properties)
22
+ return if self.class.properties.empty?
23
+ # TODO: cache the default object
24
+ self.class.properties.each do |property|
25
+ key = property.name.to_s
26
+ # let's make sure we have a default and we can assign the value
27
+ if property.default && (self.respond_to?("#{key}=") || self.key?(key))
28
+ if property.default.class == Proc
29
+ self[key] = property.default.call
30
+ else
31
+ self[key] = Marshal.load(Marshal.dump(property.default))
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def cast_keys
38
+ return unless self.class.properties
39
+ self.class.properties.each do |property|
40
+ next unless property.casted
41
+ key = self.has_key?(property.name) ? property.name : property.name.to_sym
42
+ target = property.type
43
+ if target.is_a?(Array)
44
+ next unless self[key]
45
+ klass = ::CouchRest.constantize(target[0])
46
+ self[property.name] = self[key].collect do |value|
47
+ # Auto parse Time objects
48
+ obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value)
49
+ obj.casted_by = self if obj.respond_to?(:casted_by)
50
+ obj
51
+ end
52
+ else
53
+ # Auto parse Time objects
54
+ self[property.name] = if ((property.init_method == 'new') && target == 'Time')
55
+ self[key].is_a?(String) ? Time.parse(self[key].dup) : self[key]
56
+ else
57
+ # Let people use :send as a Time parse arg
58
+ klass = ::CouchRest.constantize(target)
59
+ # I'm not convince we should or should not create a new instance if we are casting a doc/extended doc without default value and nothing was passed
60
+ # unless (property.casted &&
61
+ # (klass.superclass == CouchRest::ExtendedDocument || klass.superclass == CouchRest::Document) &&
62
+ # (self[key].nil? || property.default.nil?))
63
+ klass.send(property.init_method, self[key])
64
+ #end
65
+ end
66
+ self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by)
67
+ end
68
+ end
69
+ end
70
+
71
+ module ClassMethods
72
+
73
+ def property(name, options={})
74
+ existing_property = self.properties.find{|p| p.name == name.to_s}
75
+ if existing_property.nil? || (existing_property.default != options[:default])
76
+ define_property(name, options)
77
+ end
78
+ end
79
+
80
+ protected
81
+
82
+ # This is not a thread safe operation, if you have to set new properties at runtime
83
+ # make sure to use a mutex.
84
+ def define_property(name, options={})
85
+ # check if this property is going to casted
86
+ options[:casted] = options[:cast_as] ? options[:cast_as] : false
87
+ property = CouchRest::Property.new(name, (options.delete(:cast_as) || options.delete(:type)), options)
88
+ create_property_getter(property)
89
+ create_property_setter(property) unless property.read_only == true
90
+ properties << property
91
+ end
92
+
93
+ # defines the getter for the property (and optional aliases)
94
+ def create_property_getter(property)
95
+ # meth = property.name
96
+ class_eval <<-EOS, __FILE__, __LINE__
97
+ def #{property.name}
98
+ self['#{property.name}']
99
+ end
100
+ EOS
101
+
102
+ if property.alias
103
+ class_eval <<-EOS, __FILE__, __LINE__
104
+ alias #{property.alias.to_sym} #{property.name.to_sym}
105
+ EOS
106
+ end
107
+ end
108
+
109
+ # defines the setter for the property (and optional aliases)
110
+ def create_property_setter(property)
111
+ meth = property.name
112
+ class_eval <<-EOS
113
+ def #{meth}=(value)
114
+ self['#{meth}'] = value
115
+ end
116
+ EOS
117
+
118
+ if property.alias
119
+ class_eval <<-EOS
120
+ alias #{property.alias.to_sym}= #{meth.to_sym}=
121
+ EOS
122
+ end
123
+ end
124
+
125
+ end # module ClassMethods
126
+
127
+ end
128
+ end
129
+ end