andrewtimberlake-couch_potato 0.2.8.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE.txt +19 -0
- data/README.md +279 -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 +105 -0
- data/lib/couch_potato/persistence.rb +96 -0
- data/lib/couch_potato/persistence/belongs_to_property.rb +58 -0
- data/lib/couch_potato/persistence/callbacks.rb +60 -0
- data/lib/couch_potato/persistence/dirty_attributes.rb +27 -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 +57 -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/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/fixtures/address.rb +9 -0
- data/spec/fixtures/person.rb +6 -0
- data/spec/property_spec.rb +83 -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/callbacks_spec.rb +33 -0
- data/spec/unit/create_spec.rb +58 -0
- data/spec/unit/customs_views_spec.rb +15 -0
- data/spec/unit/database_spec.rb +38 -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 +144 -0
@@ -0,0 +1,96 @@
|
|
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/validation'
|
9
|
+
require File.dirname(__FILE__) + '/view/custom_views'
|
10
|
+
require File.dirname(__FILE__) + '/view/view_query'
|
11
|
+
|
12
|
+
|
13
|
+
module CouchPotato
|
14
|
+
module Persistence
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
base.send :include, Properties, Callbacks, Validation, Json, CouchPotato::View::CustomViews
|
18
|
+
base.send :include, DirtyAttributes
|
19
|
+
base.send :include, MagicTimestamps
|
20
|
+
base.class_eval do
|
21
|
+
attr_accessor :_id, :_rev, :_attachments, :_deleted, :database
|
22
|
+
alias_method :id, :_id
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# initialize a new instance of the model optionally passing it a hash of attributes.
|
27
|
+
# the attributes have to be declared using the #property method
|
28
|
+
#
|
29
|
+
# example:
|
30
|
+
# class Book
|
31
|
+
# include CouchPotato::Persistence
|
32
|
+
# property :title
|
33
|
+
# end
|
34
|
+
# book = Book.new :title => 'Time to Relax'
|
35
|
+
# book.title # => 'Time to Relax'
|
36
|
+
def initialize(attributes = {})
|
37
|
+
attributes.each do |name, value|
|
38
|
+
self.send("#{name}=", value)
|
39
|
+
end if attributes
|
40
|
+
end
|
41
|
+
|
42
|
+
# assign multiple attributes at once.
|
43
|
+
# the attributes have to be declared using the #property method
|
44
|
+
#
|
45
|
+
# example:
|
46
|
+
# class Book
|
47
|
+
# include CouchPotato::Persistence
|
48
|
+
# property :title
|
49
|
+
# property :year
|
50
|
+
# end
|
51
|
+
# book = Book.new
|
52
|
+
# book.attributes = {:title => 'Time to Relax', :year => 2009}
|
53
|
+
# book.title # => 'Time to Relax'
|
54
|
+
# book.year # => 2009
|
55
|
+
def attributes=(hash)
|
56
|
+
hash.each do |attribute, value|
|
57
|
+
self.send "#{attribute}=", value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# returns all of a model's attributes that have been defined using the #property method as a Hash
|
62
|
+
#
|
63
|
+
# example:
|
64
|
+
# class Book
|
65
|
+
# include CouchPotato::Persistence
|
66
|
+
# property :title
|
67
|
+
# property :year
|
68
|
+
# end
|
69
|
+
# book = Book.new :year => 2009
|
70
|
+
# book.attributes # => {:title => nil, :year => 2009}
|
71
|
+
def attributes
|
72
|
+
self.class.properties.inject({}) do |res, property|
|
73
|
+
property.serialize(res, self)
|
74
|
+
res
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# returns true if a model hasn't been saved yet, false otherwise
|
79
|
+
def new?
|
80
|
+
_rev.nil?
|
81
|
+
end
|
82
|
+
alias_method :new_record?, :new?
|
83
|
+
|
84
|
+
# returns the document id
|
85
|
+
# this is used by rails to construct URLs
|
86
|
+
# can be overridden to for example use slugs for URLs instead if ids
|
87
|
+
def to_param
|
88
|
+
_id
|
89
|
+
end
|
90
|
+
|
91
|
+
def ==(other) #:nodoc:
|
92
|
+
other.class == self.class && self.to_json == other.to_json
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
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,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,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,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,57 @@
|
|
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.map(&:name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def json_create(json) #:nodoc:
|
31
|
+
return if json.nil?
|
32
|
+
instance = super
|
33
|
+
instance.send(:assign_attribute_copies_for_dirty_tracking)
|
34
|
+
instance
|
35
|
+
end
|
36
|
+
|
37
|
+
# 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.
|
38
|
+
#
|
39
|
+
# example:
|
40
|
+
# class Book
|
41
|
+
# property :title
|
42
|
+
# property :year
|
43
|
+
# property :publisher, :class => Publisher
|
44
|
+
# end
|
45
|
+
def property(name, options = {})
|
46
|
+
clazz = options.delete(:class)
|
47
|
+
properties << (clazz || SimpleProperty).new(self, name, options)
|
48
|
+
end
|
49
|
+
|
50
|
+
def belongs_to(name) #:nodoc:
|
51
|
+
property name, :class => BelongsToProperty
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,83 @@
|
|
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}_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
|
33
|
+
value = self.instance_variable_get("@#{name}")
|
34
|
+
default = options[:default]
|
35
|
+
value.blank? ? default : value
|
36
|
+
end
|
37
|
+
|
38
|
+
define_method "#{name}=" do |value|
|
39
|
+
self.instance_variable_set("@#{name}", value)
|
40
|
+
end
|
41
|
+
|
42
|
+
define_method "#{name}?" do
|
43
|
+
!self.send(name).nil? && !self.send(name).try(:blank?)
|
44
|
+
end
|
45
|
+
|
46
|
+
define_method "#{name}_changed?" do
|
47
|
+
!self.instance_variable_get("@#{name}_not_changed") && self.send(name) != self.send("#{name}_was")
|
48
|
+
end
|
49
|
+
|
50
|
+
define_method "#{name}_not_changed" do
|
51
|
+
self.instance_variable_set("@#{name}_not_changed", true)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def build(object, json)
|
57
|
+
value = json[name.to_s] || json[name.to_sym]
|
58
|
+
typecasted_value = if type
|
59
|
+
type.json_create value
|
60
|
+
else
|
61
|
+
value
|
62
|
+
end
|
63
|
+
object.send "#{name}=", typecasted_value
|
64
|
+
end
|
65
|
+
|
66
|
+
def dirty?(object)
|
67
|
+
object.send("#{name}_changed?")
|
68
|
+
end
|
69
|
+
|
70
|
+
def save(object)
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
def destroy(object)
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
def serialize(json, object)
|
79
|
+
json[name] = object.send name
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|