couch_potato 0.2.12

Sign up to get free protection for your applications and to get access to all the features.
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