couch_potato 0.2.12

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 (52) hide show
  1. data/CHANGES.md +15 -0
  2. data/MIT-LICENSE.txt +19 -0
  3. data/README.md +295 -0
  4. data/VERSION.yml +4 -0
  5. data/init.rb +3 -0
  6. data/lib/core_ext/date.rb +10 -0
  7. data/lib/core_ext/object.rb +5 -0
  8. data/lib/core_ext/string.rb +19 -0
  9. data/lib/core_ext/symbol.rb +15 -0
  10. data/lib/core_ext/time.rb +11 -0
  11. data/lib/couch_potato.rb +40 -0
  12. data/lib/couch_potato/database.rb +106 -0
  13. data/lib/couch_potato/persistence.rb +98 -0
  14. data/lib/couch_potato/persistence/attachments.rb +31 -0
  15. data/lib/couch_potato/persistence/callbacks.rb +60 -0
  16. data/lib/couch_potato/persistence/dirty_attributes.rb +49 -0
  17. data/lib/couch_potato/persistence/ghost_attributes.rb +22 -0
  18. data/lib/couch_potato/persistence/json.rb +46 -0
  19. data/lib/couch_potato/persistence/magic_timestamps.rb +13 -0
  20. data/lib/couch_potato/persistence/properties.rb +52 -0
  21. data/lib/couch_potato/persistence/simple_property.rb +83 -0
  22. data/lib/couch_potato/persistence/validation.rb +18 -0
  23. data/lib/couch_potato/view/base_view_spec.rb +24 -0
  24. data/lib/couch_potato/view/custom_view_spec.rb +27 -0
  25. data/lib/couch_potato/view/custom_views.rb +44 -0
  26. data/lib/couch_potato/view/model_view_spec.rb +63 -0
  27. data/lib/couch_potato/view/properties_view_spec.rb +39 -0
  28. data/lib/couch_potato/view/raw_view_spec.rb +25 -0
  29. data/lib/couch_potato/view/view_query.rb +44 -0
  30. data/rails/init.rb +7 -0
  31. data/spec/attachments_spec.rb +23 -0
  32. data/spec/callbacks_spec.rb +271 -0
  33. data/spec/create_spec.rb +22 -0
  34. data/spec/custom_view_spec.rb +149 -0
  35. data/spec/default_property_spec.rb +34 -0
  36. data/spec/destroy_spec.rb +29 -0
  37. data/spec/fixtures/address.rb +9 -0
  38. data/spec/fixtures/person.rb +6 -0
  39. data/spec/property_spec.rb +101 -0
  40. data/spec/spec.opts +4 -0
  41. data/spec/spec_helper.rb +29 -0
  42. data/spec/unit/attributes_spec.rb +48 -0
  43. data/spec/unit/callbacks_spec.rb +33 -0
  44. data/spec/unit/couch_potato_spec.rb +20 -0
  45. data/spec/unit/create_spec.rb +58 -0
  46. data/spec/unit/customs_views_spec.rb +15 -0
  47. data/spec/unit/database_spec.rb +50 -0
  48. data/spec/unit/dirty_attributes_spec.rb +131 -0
  49. data/spec/unit/string_spec.rb +13 -0
  50. data/spec/unit/view_query_spec.rb +9 -0
  51. data/spec/update_spec.rb +40 -0
  52. metadata +153 -0
@@ -0,0 +1,106 @@
1
+ module CouchPotato
2
+ class Database
3
+
4
+ class ValidationsFailedError < ::StandardError; end
5
+
6
+ def initialize(couchrest_database)
7
+ @database = couchrest_database
8
+ begin
9
+ couchrest_database.info
10
+ rescue RestClient::ResourceNotFound
11
+ raise "Database '#{couchrest_database.name}' does not exist."
12
+ end
13
+ end
14
+
15
+ def view(spec)
16
+ results = CouchPotato::View::ViewQuery.new(database,
17
+ spec.design_document, spec.view_name, spec.map_function,
18
+ spec.reduce_function).query_view!(spec.view_parameters)
19
+ spec.process_results results
20
+ end
21
+
22
+ def save_document(document)
23
+ return true unless document.dirty?
24
+ if document.new?
25
+ create_document document
26
+ else
27
+ update_document document
28
+ end
29
+ end
30
+ alias_method :save, :save_document
31
+
32
+ def save_document!(document)
33
+ save_document(document) || raise(ValidationsFailedError.new(document.errors.full_messages))
34
+ end
35
+ alias_method :save!, :save_document!
36
+
37
+ def destroy_document(document)
38
+ document.run_callbacks :before_destroy
39
+ document._deleted = true
40
+ database.delete_doc document.to_hash
41
+ document.run_callbacks :after_destroy
42
+ document._id = nil
43
+ document._rev = nil
44
+ end
45
+ alias_method :destroy, :destroy_document
46
+
47
+ def load_document(id)
48
+ raise "Can't load a document without an id (got nil)" if id.nil?
49
+ begin
50
+ json = database.get(id)
51
+ klass = json['ruby_class'].split('::').inject(Class){|scope, const| scope.const_get(const)}
52
+ instance = klass.json_create json
53
+ instance.database = self
54
+ instance
55
+ rescue(RestClient::ResourceNotFound)
56
+ nil
57
+ end
58
+ end
59
+ alias_method :load, :load_document
60
+
61
+ def inspect
62
+ "#<CouchPotato::Database>"
63
+ end
64
+
65
+ private
66
+
67
+ def clean_hash(hash)
68
+ hash.each do |k,v|
69
+ hash.delete k unless v
70
+ end
71
+ end
72
+
73
+ def create_document(document)
74
+ document.database = self
75
+ document.run_callbacks :before_validation_on_save
76
+ document.run_callbacks :before_validation_on_create
77
+ return unless document.valid?
78
+ document.run_callbacks :before_save
79
+ document.run_callbacks :before_create
80
+ res = database.save_doc clean_hash(document.to_hash)
81
+ document._rev = res['rev']
82
+ document._id = res['id']
83
+ document.run_callbacks :after_save
84
+ document.run_callbacks :after_create
85
+ true
86
+ end
87
+
88
+ def update_document(document)
89
+ document.run_callbacks :before_validation_on_save
90
+ document.run_callbacks :before_validation_on_update
91
+ return unless document.valid?
92
+ document.run_callbacks :before_save
93
+ document.run_callbacks :before_update
94
+ res = database.save_doc clean_hash(document.to_hash)
95
+ document._rev = res['rev']
96
+ document.run_callbacks :after_save
97
+ document.run_callbacks :after_update
98
+ true
99
+ end
100
+
101
+ def database
102
+ @database
103
+ end
104
+
105
+ end
106
+ end
@@ -0,0 +1,98 @@
1
+ require 'digest/md5'
2
+ require File.dirname(__FILE__) + '/database'
3
+ require File.dirname(__FILE__) + '/persistence/properties'
4
+ require File.dirname(__FILE__) + '/persistence/magic_timestamps'
5
+ require File.dirname(__FILE__) + '/persistence/callbacks'
6
+ require File.dirname(__FILE__) + '/persistence/json'
7
+ require File.dirname(__FILE__) + '/persistence/dirty_attributes'
8
+ require File.dirname(__FILE__) + '/persistence/ghost_attributes'
9
+ require File.dirname(__FILE__) + '/persistence/attachments'
10
+ require File.dirname(__FILE__) + '/persistence/validation'
11
+ require File.dirname(__FILE__) + '/view/custom_views'
12
+ require File.dirname(__FILE__) + '/view/view_query'
13
+
14
+
15
+ module CouchPotato
16
+ module Persistence
17
+
18
+ def self.included(base)
19
+ base.send :include, Properties, Callbacks, Validation, Json, CouchPotato::View::CustomViews
20
+ base.send :include, DirtyAttributes, GhostAttributes, Attachments
21
+ base.send :include, MagicTimestamps
22
+ base.class_eval do
23
+ attr_accessor :_id, :_rev, :_deleted, :database
24
+ alias_method :id, :_id
25
+ end
26
+ end
27
+
28
+ # initialize a new instance of the model optionally passing it a hash of attributes.
29
+ # the attributes have to be declared using the #property method
30
+ #
31
+ # example:
32
+ # class Book
33
+ # include CouchPotato::Persistence
34
+ # property :title
35
+ # end
36
+ # book = Book.new :title => 'Time to Relax'
37
+ # book.title # => 'Time to Relax'
38
+ def initialize(attributes = {})
39
+ attributes.each do |name, value|
40
+ self.send("#{name}=", value)
41
+ end if attributes
42
+ end
43
+
44
+ # assign multiple attributes at once.
45
+ # the attributes have to be declared using the #property method
46
+ #
47
+ # example:
48
+ # class Book
49
+ # include CouchPotato::Persistence
50
+ # property :title
51
+ # property :year
52
+ # end
53
+ # book = Book.new
54
+ # book.attributes = {:title => 'Time to Relax', :year => 2009}
55
+ # book.title # => 'Time to Relax'
56
+ # book.year # => 2009
57
+ def attributes=(hash)
58
+ hash.each do |attribute, value|
59
+ self.send "#{attribute}=", value
60
+ end
61
+ end
62
+
63
+ # returns all of a model's attributes that have been defined using the #property method as a Hash
64
+ #
65
+ # example:
66
+ # class Book
67
+ # include CouchPotato::Persistence
68
+ # property :title
69
+ # property :year
70
+ # end
71
+ # book = Book.new :year => 2009
72
+ # book.attributes # => {:title => nil, :year => 2009}
73
+ def attributes
74
+ self.class.properties.inject({}) do |res, property|
75
+ property.serialize(res, self)
76
+ res
77
+ end
78
+ end
79
+
80
+ # returns true if a model hasn't been saved yet, false otherwise
81
+ def new?
82
+ _rev.nil?
83
+ end
84
+ alias_method :new_record?, :new?
85
+
86
+ # returns the document id
87
+ # this is used by rails to construct URLs
88
+ # can be overridden to for example use slugs for URLs instead if ids
89
+ def to_param
90
+ _id
91
+ end
92
+
93
+ def ==(other) #:nodoc:
94
+ other.class == self.class && self.to_json == other.to_json
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,31 @@
1
+ module CouchPotato
2
+ module Attachments
3
+ def self.included(base)
4
+ base.class_eval do
5
+ attr_writer :_attachments
6
+
7
+ def _attachments
8
+ @_attachments || {}
9
+ end
10
+
11
+ base.extend ClassMethods
12
+ end
13
+ end
14
+
15
+ def to_hash
16
+ if _attachments
17
+ super.merge('_attachments' => _attachments)
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ def json_create(json)
25
+ instance = super
26
+ instance._attachments = json['_attachments'] if json
27
+ instance
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,60 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ module Callbacks
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+
7
+ base.class_eval do
8
+ attr_accessor :skip_callbacks
9
+ def self.callbacks
10
+ @callbacks ||= {}
11
+ @callbacks[self.name] ||= {:before_validation => [], :before_validation_on_create => [],
12
+ :before_validation_on_update => [], :before_validation_on_save => [], :before_create => [],
13
+ :after_create => [], :before_update => [], :after_update => [],
14
+ :before_save => [], :after_save => [],
15
+ :before_destroy => [], :after_destroy => []}
16
+ end
17
+ end
18
+ end
19
+
20
+ # Runs all callbacks on a model with the given name, i.g. :after_create.
21
+ #
22
+ # This method is called by the CouchPotato::Database object when saving/destroying an object
23
+ def run_callbacks(name)
24
+ return if skip_callbacks
25
+ self.class.callbacks[name].uniq.each do |callback|
26
+ if callback.is_a?(Symbol)
27
+ send callback
28
+ elsif callback.is_a?(Proc)
29
+ callback.call self
30
+ else
31
+ raise "Don't know how to handle callback of type #{name.class.name}"
32
+ end
33
+ end
34
+ end
35
+
36
+ module ClassMethods
37
+ [
38
+ :before_validation,
39
+ :before_validation_on_create,
40
+ :before_validation_on_update,
41
+ :before_validation_on_save,
42
+ :before_create,
43
+ :before_save,
44
+ :before_update,
45
+ :before_destroy,
46
+ :after_update,
47
+ :after_save,
48
+ :after_create,
49
+ :after_destroy
50
+ ].each do |callback|
51
+ define_method callback do |*names|
52
+ names.each do |name|
53
+ callbacks[callback] << name
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,49 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ module DirtyAttributes
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ after_save :reset_dirty_attributes
8
+
9
+ def initialize(attributes = {})
10
+ super
11
+ assign_attribute_copies_for_dirty_tracking
12
+ end
13
+ end
14
+ end
15
+
16
+ # returns true if a model has dirty attributes, i.e. their value has changed since the last save
17
+ def dirty?
18
+ new? || self.class.properties.inject(false) do |res, property|
19
+ res || property.dirty?(self)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def assign_attribute_copies_for_dirty_tracking
26
+ attributes.each do |name, value|
27
+ self.instance_variable_set("@#{name}_was", clone_attribute(value))
28
+ end if attributes
29
+ end
30
+
31
+ def reset_dirty_attributes
32
+ self.class.properties.each do |property|
33
+ instance_variable_set("@#{property.name}_was", clone_attribute(send(property.name)))
34
+ end
35
+ end
36
+
37
+ def clone_attribute(value)
38
+ if [Fixnum, Symbol, TrueClass, FalseClass, NilClass, Float].include?(value.class)
39
+ value
40
+ elsif [Hash, Array].include?(value.class)
41
+ #Deep clone
42
+ Marshal::load(Marshal::dump(value))
43
+ else
44
+ value.clone
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,22 @@
1
+ module CouchPotato
2
+ module GhostAttributes
3
+ def self.included(base)
4
+ base.class_eval do
5
+ attr_accessor :_document
6
+ def self.json_create(json)
7
+ instance = super
8
+ instance._document = json if json
9
+ instance
10
+ end
11
+
12
+ def method_missing(name, *args)
13
+ if(value = _document && _document[name.to_s])
14
+ value
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,46 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ module Json
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ # returns a JSON representation of a model in order to store it in CouchDB
9
+ def to_json(*args)
10
+ to_hash.to_json(*args)
11
+ end
12
+
13
+ # returns all the attributes, the ruby class and the _id and _rev of a model as a Hash
14
+ def to_hash
15
+ (self.class.properties).inject({}) do |props, property|
16
+ property.serialize(props, self)
17
+ props
18
+ end.merge('ruby_class' => self.class.name).merge(id_and_rev_json)
19
+ end
20
+
21
+ private
22
+
23
+ def id_and_rev_json
24
+ ['_id', '_rev', '_deleted'].inject({}) do |hash, key|
25
+ hash[key] = self.send(key) unless self.send(key).nil?
26
+ hash
27
+ end
28
+ end
29
+
30
+ module ClassMethods
31
+
32
+ # creates a model instance from JSON
33
+ def json_create(json)
34
+ return if json.nil?
35
+ instance = self.new
36
+ instance._id = json[:_id] || json['_id']
37
+ instance._rev = json[:_rev] || json['_rev']
38
+ properties.each do |property|
39
+ property.build(instance, json)
40
+ end
41
+ instance
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ module CouchPotato
2
+ module MagicTimestamps #:nodoc:
3
+ def self.included(base)
4
+ base.instance_eval do
5
+ property :created_at, :type => Time
6
+ property :updated_at, :type => Time
7
+
8
+ before_create lambda {|model| model.created_at = Time.now; model.created_at_not_changed}
9
+ before_save lambda {|model| model.updated_at = Time.now; model.updated_at_not_changed}
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/simple_property'
2
+
3
+ module CouchPotato
4
+ module Persistence
5
+ module Properties
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ base.class_eval do
9
+ def self.properties
10
+ @properties ||= {}
11
+ @properties[self.name] ||= []
12
+ end
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ # returns all the property names of a model class that have been defined using the #property method
18
+ #
19
+ # example:
20
+ # class Book
21
+ # property :title
22
+ # property :year
23
+ # end
24
+ # Book.property_names # => [:title, :year]
25
+ def property_names
26
+ properties.map(&:name)
27
+ end
28
+
29
+ def json_create(json) #:nodoc:
30
+ return if json.nil?
31
+ instance = super
32
+ instance.send(:assign_attribute_copies_for_dirty_tracking)
33
+ instance
34
+ end
35
+
36
+ # Declare a proprty on a model class. properties are not typed by default. You can use any of the basic types by JSON (String, Integer, Fixnum, Array, Hash). If you want a property to be of a custom class you have to define it using the :class option.
37
+ #
38
+ # example:
39
+ # class Book
40
+ # property :title
41
+ # property :year
42
+ # property :publisher, :class => Publisher
43
+ # end
44
+ def property(name, options = {})
45
+ clazz = options.delete(:class)
46
+ properties << (clazz || SimpleProperty).new(self, name, options)
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+ end