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