thefool808-couch_potato 0.2.7
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/MIT-LICENSE.txt +19 -0
- data/README.md +269 -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 +15 -0
- data/lib/core_ext/time.rb +11 -0
- data/lib/couch_potato.rb +41 -0
- data/lib/couch_potato/database.rb +96 -0
- data/lib/couch_potato/persistence.rb +94 -0
- data/lib/couch_potato/persistence/belongs_to_property.rb +58 -0
- data/lib/couch_potato/persistence/callbacks.rb +96 -0
- data/lib/couch_potato/persistence/dirty_attributes.rb +27 -0
- data/lib/couch_potato/persistence/json.rb +45 -0
- data/lib/couch_potato/persistence/magic_timestamps.rb +13 -0
- data/lib/couch_potato/persistence/properties.rb +56 -0
- data/lib/couch_potato/persistence/simple_property.rb +77 -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/callbacks_spec.rb +271 -0
- data/spec/create_spec.rb +22 -0
- data/spec/custom_view_spec.rb +134 -0
- data/spec/destroy_spec.rb +29 -0
- data/spec/property_spec.rb +64 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/unit/attributes_spec.rb +26 -0
- data/spec/unit/create_spec.rb +58 -0
- data/spec/unit/customs_views_spec.rb +15 -0
- data/spec/unit/database_spec.rb +18 -0
- data/spec/unit/dirty_attributes_spec.rb +113 -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 +135 -0
|
@@ -0,0 +1,94 @@
|
|
|
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__) + '/view/custom_views'
|
|
9
|
+
require File.dirname(__FILE__) + '/view/view_query'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
module CouchPotato
|
|
13
|
+
module Persistence
|
|
14
|
+
|
|
15
|
+
def self.included(base)
|
|
16
|
+
base.send :include, Properties, Callbacks, Validatable, Json, CouchPotato::View::CustomViews
|
|
17
|
+
base.send :include, DirtyAttributes
|
|
18
|
+
base.send :include, MagicTimestamps
|
|
19
|
+
base.class_eval do
|
|
20
|
+
attr_accessor :_id, :_rev, :_attachments, :_deleted
|
|
21
|
+
alias_method :id, :_id
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# initialize a new instance of the model optionally passing it a hash of attributes.
|
|
26
|
+
# the attributes have to be declared using the #property method
|
|
27
|
+
#
|
|
28
|
+
# example:
|
|
29
|
+
# class Book
|
|
30
|
+
# include CouchPotato::Persistence
|
|
31
|
+
# property :title
|
|
32
|
+
# end
|
|
33
|
+
# book = Book.new :title => 'Time to Relax'
|
|
34
|
+
# book.title # => 'Time to Relax'
|
|
35
|
+
def initialize(attributes = {})
|
|
36
|
+
attributes.each do |name, value|
|
|
37
|
+
self.send("#{name}=", value)
|
|
38
|
+
end if attributes
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# assign multiple attributes at once.
|
|
42
|
+
# the attributes have to be declared using the #property method
|
|
43
|
+
#
|
|
44
|
+
# example:
|
|
45
|
+
# class Book
|
|
46
|
+
# include CouchPotato::Persistence
|
|
47
|
+
# property :title
|
|
48
|
+
# property :year
|
|
49
|
+
# end
|
|
50
|
+
# book = Book.new
|
|
51
|
+
# book.attributes = {:title => 'Time to Relax', :year => 2009}
|
|
52
|
+
# book.title # => 'Time to Relax'
|
|
53
|
+
# book.year # => 2009
|
|
54
|
+
def attributes=(hash)
|
|
55
|
+
hash.each do |attribute, value|
|
|
56
|
+
self.send "#{attribute}=", value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# returns all of a model's attributes that have been defined using the #property method as a Hash
|
|
61
|
+
#
|
|
62
|
+
# example:
|
|
63
|
+
# class Book
|
|
64
|
+
# include CouchPotato::Persistence
|
|
65
|
+
# property :title
|
|
66
|
+
# property :year
|
|
67
|
+
# end
|
|
68
|
+
# book = Book.new :year => 2009
|
|
69
|
+
# book.attributes # => {:title => nil, :year => 2009}
|
|
70
|
+
def attributes
|
|
71
|
+
self.class.properties.inject({}) do |res, property|
|
|
72
|
+
property.serialize(res, self)
|
|
73
|
+
res
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# returns true if a model hasn't been saved yet, false otherwise
|
|
78
|
+
def new?
|
|
79
|
+
_rev.nil?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# returns the document id
|
|
83
|
+
# this is used by rails to construct URLs
|
|
84
|
+
# can be overridden to for example use slugs for URLs instead if ids
|
|
85
|
+
def to_param
|
|
86
|
+
_id
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ==(other) #:nodoc:
|
|
90
|
+
other.class == self.class && self.to_json == other.to_json
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module CouchPotato
|
|
2
|
+
module Persistence
|
|
3
|
+
class BelongsToProperty #:nodoc:
|
|
4
|
+
attr_accessor :name
|
|
5
|
+
|
|
6
|
+
def initialize(owner_clazz, name, options = {})
|
|
7
|
+
self.name = name
|
|
8
|
+
accessors = <<-ACCESSORS
|
|
9
|
+
def #{name}
|
|
10
|
+
return @#{name} if instance_variable_defined?(:@#{name})
|
|
11
|
+
@#{name} = @#{name}_id ? #{item_class_name}.find(@#{name}_id) : nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def #{name}=(value)
|
|
15
|
+
@#{name} = value
|
|
16
|
+
if value.nil?
|
|
17
|
+
@#{name}_id = nil
|
|
18
|
+
else
|
|
19
|
+
@#{name}_id = value.id
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def #{name}_id=(id)
|
|
24
|
+
remove_instance_variable(:@#{name}) if instance_variable_defined?(:@#{name})
|
|
25
|
+
@#{name}_id = id
|
|
26
|
+
end
|
|
27
|
+
ACCESSORS
|
|
28
|
+
owner_clazz.class_eval accessors
|
|
29
|
+
owner_clazz.send :attr_reader, "#{name}_id"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def save(object)
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dirty?(object)
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def destroy(object)
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build(object, json)
|
|
45
|
+
object.send "#{name}_id=", json["#{name}_id"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def serialize(json, object)
|
|
49
|
+
json["#{name}_id"] = object.send("#{name}_id") if object.send("#{name}_id")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def item_class_name
|
|
53
|
+
@name.to_s.camelize
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module CouchPotato
|
|
2
|
+
module Persistence
|
|
3
|
+
module Callbacks
|
|
4
|
+
|
|
5
|
+
class Callback #:nodoc:
|
|
6
|
+
def initialize(model, name, database)
|
|
7
|
+
@model, @name, @database = model, name, database
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
if @name.is_a?(Symbol)
|
|
12
|
+
run_method_callback @name
|
|
13
|
+
elsif @name.is_a?(Proc)
|
|
14
|
+
run_lambda_callback @name
|
|
15
|
+
else
|
|
16
|
+
raise "Don't know how to handle callback of type #{name.class.name}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def run_method_callback(name)
|
|
23
|
+
if [-1, 0].include? callback_method(name).arity
|
|
24
|
+
@model.send name
|
|
25
|
+
elsif callback_method(name).arity == 1
|
|
26
|
+
@model.send name, @database
|
|
27
|
+
else
|
|
28
|
+
raise "Don't know how to handle method callback with #{callback_method(name).arity} arguments"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def callback_method(name)
|
|
33
|
+
@model.method(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run_lambda_callback(lambda)
|
|
37
|
+
if lambda.arity == 1
|
|
38
|
+
lambda.call @model
|
|
39
|
+
elsif lambda.arity == 2
|
|
40
|
+
lambda.call @model, @database
|
|
41
|
+
else raise "Don't know how to handle lambda callback with #{lambda.arity} arguments"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.included(base)
|
|
48
|
+
base.extend ClassMethods
|
|
49
|
+
|
|
50
|
+
base.class_eval do
|
|
51
|
+
attr_accessor :skip_callbacks
|
|
52
|
+
def self.callbacks
|
|
53
|
+
@callbacks ||= {}
|
|
54
|
+
@callbacks[self.name] ||= {:before_validation_on_create => [],
|
|
55
|
+
:before_validation_on_update => [], :before_validation_on_save => [], :before_create => [],
|
|
56
|
+
:after_create => [], :before_update => [], :after_update => [],
|
|
57
|
+
:before_save => [], :after_save => [],
|
|
58
|
+
:before_destroy => [], :after_destroy => []}
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Runs all callbacks on a model with the given name, i.g. :after_create.
|
|
64
|
+
#
|
|
65
|
+
# This method is called by the CouchPotato::Database object when saving/destroying an object
|
|
66
|
+
def run_callbacks(name, database)
|
|
67
|
+
return if skip_callbacks
|
|
68
|
+
self.class.callbacks[name].uniq.each do |callback|
|
|
69
|
+
Callback.new(self, callback, database).run
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
module ClassMethods
|
|
74
|
+
[
|
|
75
|
+
:before_validation_on_create,
|
|
76
|
+
:before_validation_on_update,
|
|
77
|
+
:before_validation_on_save,
|
|
78
|
+
:before_create,
|
|
79
|
+
:before_save,
|
|
80
|
+
:before_update,
|
|
81
|
+
:before_destroy,
|
|
82
|
+
:after_update,
|
|
83
|
+
:after_save,
|
|
84
|
+
:after_create,
|
|
85
|
+
:after_destroy
|
|
86
|
+
].each do |callback|
|
|
87
|
+
define_method callback do |*names|
|
|
88
|
+
names.each do |name|
|
|
89
|
+
callbacks[callback] << name
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# returns true if a model has dirty attributes, i.e. their value has changed since the last save
|
|
12
|
+
def dirty?
|
|
13
|
+
new? || self.class.properties.inject(false) do |res, property|
|
|
14
|
+
res || property.dirty?(self)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def reset_dirty_attributes
|
|
21
|
+
self.class.properties.each do |property|
|
|
22
|
+
instance_variable_set("@#{property.name}_was", send(property.name))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
instance = self.new
|
|
35
|
+
instance._id = json[:_id] || json['_id']
|
|
36
|
+
instance._rev = json[:_rev] || json['_rev']
|
|
37
|
+
properties.each do |property|
|
|
38
|
+
property.build(instance, json)
|
|
39
|
+
end
|
|
40
|
+
instance
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
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,56 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/simple_property'
|
|
2
|
+
require File.dirname(__FILE__) + '/belongs_to_property'
|
|
3
|
+
|
|
4
|
+
module CouchPotato
|
|
5
|
+
module Persistence
|
|
6
|
+
module Properties
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.extend ClassMethods
|
|
9
|
+
base.class_eval do
|
|
10
|
+
def self.properties
|
|
11
|
+
@properties ||= {}
|
|
12
|
+
@properties[self.name] ||= []
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
# returns all the property names of a model class that have been defined using the #property method
|
|
19
|
+
#
|
|
20
|
+
# example:
|
|
21
|
+
# class Book
|
|
22
|
+
# property :title
|
|
23
|
+
# property :year
|
|
24
|
+
# end
|
|
25
|
+
# Book.property_names # => [:title, :year]
|
|
26
|
+
def property_names
|
|
27
|
+
properties.collect{|prop| prop.name}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def json_create(json) #:nodoc:
|
|
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
|
+
def belongs_to(name) #:nodoc:
|
|
50
|
+
property name, :class => BelongsToProperty
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module CouchPotato
|
|
2
|
+
module Persistence
|
|
3
|
+
class SimpleProperty #:nodoc:
|
|
4
|
+
attr_accessor :name, :type
|
|
5
|
+
|
|
6
|
+
def initialize(owner_clazz, name, options = {})
|
|
7
|
+
self.name = name
|
|
8
|
+
self.type = options[:type]
|
|
9
|
+
owner_clazz.class_eval do
|
|
10
|
+
attr_reader name, "#{name}_was"
|
|
11
|
+
|
|
12
|
+
def initialize(attributes = {})
|
|
13
|
+
super attributes
|
|
14
|
+
assign_attribute_copies_for_dirty_tracking
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def assign_attribute_copies_for_dirty_tracking
|
|
18
|
+
attributes.each do |name, value|
|
|
19
|
+
self.instance_variable_set("@#{name}_was", clone_attribute(value))
|
|
20
|
+
end if attributes
|
|
21
|
+
end
|
|
22
|
+
private :assign_attribute_copies_for_dirty_tracking
|
|
23
|
+
|
|
24
|
+
def clone_attribute(value)
|
|
25
|
+
if [Fixnum, Symbol, TrueClass, FalseClass, NilClass, Float].include?(value.class)
|
|
26
|
+
value
|
|
27
|
+
else
|
|
28
|
+
value.clone
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
define_method "#{name}=" do |value|
|
|
33
|
+
self.instance_variable_set("@#{name}", value)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
define_method "#{name}?" do
|
|
37
|
+
!self.send(name).nil? && !self.send(name).try(:blank?)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
define_method "#{name}_changed?" do
|
|
41
|
+
!self.instance_variable_get("@#{name}_not_changed") && self.send(name) != self.send("#{name}_was")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
define_method "#{name}_not_changed" do
|
|
45
|
+
self.instance_variable_set("@#{name}_not_changed", true)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build(object, json)
|
|
51
|
+
value = json[name.to_s] || json[name.to_sym]
|
|
52
|
+
typecasted_value = if type
|
|
53
|
+
type.json_create value
|
|
54
|
+
else
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
object.send "#{name}=", typecasted_value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def dirty?(object)
|
|
61
|
+
object.send("#{name}_changed?")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def save(object)
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def destroy(object)
|
|
69
|
+
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def serialize(json, object)
|
|
73
|
+
json[name] = object.send name
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|