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.
- data/CHANGES.md +15 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.md +295 -0
- data/VERSION.yml +4 -0
- data/init.rb +3 -0
- data/lib/core_ext/date.rb +10 -0
- data/lib/core_ext/object.rb +5 -0
- data/lib/core_ext/string.rb +19 -0
- data/lib/core_ext/symbol.rb +15 -0
- data/lib/core_ext/time.rb +11 -0
- data/lib/couch_potato.rb +40 -0
- data/lib/couch_potato/database.rb +106 -0
- data/lib/couch_potato/persistence.rb +98 -0
- data/lib/couch_potato/persistence/attachments.rb +31 -0
- data/lib/couch_potato/persistence/callbacks.rb +60 -0
- data/lib/couch_potato/persistence/dirty_attributes.rb +49 -0
- data/lib/couch_potato/persistence/ghost_attributes.rb +22 -0
- data/lib/couch_potato/persistence/json.rb +46 -0
- data/lib/couch_potato/persistence/magic_timestamps.rb +13 -0
- data/lib/couch_potato/persistence/properties.rb +52 -0
- data/lib/couch_potato/persistence/simple_property.rb +83 -0
- data/lib/couch_potato/persistence/validation.rb +18 -0
- data/lib/couch_potato/view/base_view_spec.rb +24 -0
- data/lib/couch_potato/view/custom_view_spec.rb +27 -0
- data/lib/couch_potato/view/custom_views.rb +44 -0
- data/lib/couch_potato/view/model_view_spec.rb +63 -0
- data/lib/couch_potato/view/properties_view_spec.rb +39 -0
- data/lib/couch_potato/view/raw_view_spec.rb +25 -0
- data/lib/couch_potato/view/view_query.rb +44 -0
- data/rails/init.rb +7 -0
- data/spec/attachments_spec.rb +23 -0
- data/spec/callbacks_spec.rb +271 -0
- data/spec/create_spec.rb +22 -0
- data/spec/custom_view_spec.rb +149 -0
- data/spec/default_property_spec.rb +34 -0
- data/spec/destroy_spec.rb +29 -0
- data/spec/fixtures/address.rb +9 -0
- data/spec/fixtures/person.rb +6 -0
- data/spec/property_spec.rb +101 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/unit/attributes_spec.rb +48 -0
- data/spec/unit/callbacks_spec.rb +33 -0
- data/spec/unit/couch_potato_spec.rb +20 -0
- data/spec/unit/create_spec.rb +58 -0
- data/spec/unit/customs_views_spec.rb +15 -0
- data/spec/unit/database_spec.rb +50 -0
- data/spec/unit/dirty_attributes_spec.rb +131 -0
- data/spec/unit/string_spec.rb +13 -0
- data/spec/unit/view_query_spec.rb +9 -0
- data/spec/update_spec.rb +40 -0
- 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
|