brianmario-couchrest 0.23

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.
Files changed (92) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +95 -0
  3. data/Rakefile +75 -0
  4. data/THANKS.md +18 -0
  5. data/examples/model/example.rb +144 -0
  6. data/examples/word_count/markov +38 -0
  7. data/examples/word_count/views/books/chunked-map.js +3 -0
  8. data/examples/word_count/views/books/united-map.js +1 -0
  9. data/examples/word_count/views/markov/chain-map.js +6 -0
  10. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  11. data/examples/word_count/views/word_count/count-map.js +6 -0
  12. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  13. data/examples/word_count/word_count.rb +46 -0
  14. data/examples/word_count/word_count_query.rb +40 -0
  15. data/examples/word_count/word_count_views.rb +26 -0
  16. data/lib/couchrest.rb +198 -0
  17. data/lib/couchrest/commands/generate.rb +71 -0
  18. data/lib/couchrest/commands/push.rb +103 -0
  19. data/lib/couchrest/core/database.rb +303 -0
  20. data/lib/couchrest/core/design.rb +79 -0
  21. data/lib/couchrest/core/document.rb +87 -0
  22. data/lib/couchrest/core/response.rb +16 -0
  23. data/lib/couchrest/core/server.rb +88 -0
  24. data/lib/couchrest/core/view.rb +4 -0
  25. data/lib/couchrest/helper/pager.rb +103 -0
  26. data/lib/couchrest/helper/streamer.rb +44 -0
  27. data/lib/couchrest/helper/upgrade.rb +51 -0
  28. data/lib/couchrest/mixins.rb +4 -0
  29. data/lib/couchrest/mixins/attachments.rb +31 -0
  30. data/lib/couchrest/mixins/callbacks.rb +483 -0
  31. data/lib/couchrest/mixins/class_proxy.rb +108 -0
  32. data/lib/couchrest/mixins/design_doc.rb +90 -0
  33. data/lib/couchrest/mixins/document_queries.rb +44 -0
  34. data/lib/couchrest/mixins/extended_attachments.rb +68 -0
  35. data/lib/couchrest/mixins/extended_document_mixins.rb +7 -0
  36. data/lib/couchrest/mixins/properties.rb +129 -0
  37. data/lib/couchrest/mixins/validation.rb +242 -0
  38. data/lib/couchrest/mixins/views.rb +169 -0
  39. data/lib/couchrest/monkeypatches.rb +113 -0
  40. data/lib/couchrest/more/casted_model.rb +28 -0
  41. data/lib/couchrest/more/extended_document.rb +215 -0
  42. data/lib/couchrest/more/property.rb +40 -0
  43. data/lib/couchrest/support/blank.rb +42 -0
  44. data/lib/couchrest/support/class.rb +176 -0
  45. data/lib/couchrest/validation/auto_validate.rb +163 -0
  46. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  47. data/lib/couchrest/validation/validation_errors.rb +118 -0
  48. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  49. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  50. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  51. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  52. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  53. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  54. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  55. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  56. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  57. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  58. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  59. data/spec/couchrest/core/database_spec.rb +699 -0
  60. data/spec/couchrest/core/design_spec.rb +138 -0
  61. data/spec/couchrest/core/document_spec.rb +267 -0
  62. data/spec/couchrest/core/server_spec.rb +35 -0
  63. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  64. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  65. data/spec/couchrest/more/casted_extended_doc_spec.rb +40 -0
  66. data/spec/couchrest/more/casted_model_spec.rb +98 -0
  67. data/spec/couchrest/more/extended_doc_attachment_spec.rb +130 -0
  68. data/spec/couchrest/more/extended_doc_spec.rb +509 -0
  69. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  70. data/spec/couchrest/more/extended_doc_view_spec.rb +355 -0
  71. data/spec/couchrest/more/property_spec.rb +136 -0
  72. data/spec/fixtures/attachments/README +3 -0
  73. data/spec/fixtures/attachments/couchdb.png +0 -0
  74. data/spec/fixtures/attachments/test.html +11 -0
  75. data/spec/fixtures/more/article.rb +34 -0
  76. data/spec/fixtures/more/card.rb +20 -0
  77. data/spec/fixtures/more/course.rb +14 -0
  78. data/spec/fixtures/more/event.rb +6 -0
  79. data/spec/fixtures/more/invoice.rb +17 -0
  80. data/spec/fixtures/more/person.rb +8 -0
  81. data/spec/fixtures/more/question.rb +6 -0
  82. data/spec/fixtures/more/service.rb +12 -0
  83. data/spec/fixtures/views/lib.js +3 -0
  84. data/spec/fixtures/views/test_view/lib.js +3 -0
  85. data/spec/fixtures/views/test_view/only-map.js +4 -0
  86. data/spec/fixtures/views/test_view/test-map.js +3 -0
  87. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  88. data/spec/spec.opts +6 -0
  89. data/spec/spec_helper.rb +26 -0
  90. data/utils/remap.rb +27 -0
  91. data/utils/subset.rb +30 -0
  92. metadata +200 -0
@@ -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