andrewtimberlake-couch_potato 0.2.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/MIT-LICENSE.txt +19 -0
  2. data/README.md +279 -0
  3. data/VERSION.yml +4 -0
  4. data/init.rb +3 -0
  5. data/lib/core_ext/date.rb +10 -0
  6. data/lib/core_ext/object.rb +5 -0
  7. data/lib/core_ext/string.rb +19 -0
  8. data/lib/core_ext/symbol.rb +15 -0
  9. data/lib/core_ext/time.rb +11 -0
  10. data/lib/couch_potato.rb +40 -0
  11. data/lib/couch_potato/database.rb +105 -0
  12. data/lib/couch_potato/persistence.rb +96 -0
  13. data/lib/couch_potato/persistence/belongs_to_property.rb +58 -0
  14. data/lib/couch_potato/persistence/callbacks.rb +60 -0
  15. data/lib/couch_potato/persistence/dirty_attributes.rb +27 -0
  16. data/lib/couch_potato/persistence/json.rb +46 -0
  17. data/lib/couch_potato/persistence/magic_timestamps.rb +13 -0
  18. data/lib/couch_potato/persistence/properties.rb +57 -0
  19. data/lib/couch_potato/persistence/simple_property.rb +83 -0
  20. data/lib/couch_potato/persistence/validation.rb +18 -0
  21. data/lib/couch_potato/view/base_view_spec.rb +24 -0
  22. data/lib/couch_potato/view/custom_view_spec.rb +27 -0
  23. data/lib/couch_potato/view/custom_views.rb +44 -0
  24. data/lib/couch_potato/view/model_view_spec.rb +63 -0
  25. data/lib/couch_potato/view/properties_view_spec.rb +39 -0
  26. data/lib/couch_potato/view/raw_view_spec.rb +25 -0
  27. data/lib/couch_potato/view/view_query.rb +44 -0
  28. data/rails/init.rb +7 -0
  29. data/spec/callbacks_spec.rb +271 -0
  30. data/spec/create_spec.rb +22 -0
  31. data/spec/custom_view_spec.rb +134 -0
  32. data/spec/destroy_spec.rb +29 -0
  33. data/spec/fixtures/address.rb +9 -0
  34. data/spec/fixtures/person.rb +6 -0
  35. data/spec/property_spec.rb +83 -0
  36. data/spec/spec.opts +4 -0
  37. data/spec/spec_helper.rb +31 -0
  38. data/spec/unit/attributes_spec.rb +26 -0
  39. data/spec/unit/callbacks_spec.rb +33 -0
  40. data/spec/unit/create_spec.rb +58 -0
  41. data/spec/unit/customs_views_spec.rb +15 -0
  42. data/spec/unit/database_spec.rb +38 -0
  43. data/spec/unit/dirty_attributes_spec.rb +113 -0
  44. data/spec/unit/string_spec.rb +13 -0
  45. data/spec/unit/view_query_spec.rb +9 -0
  46. data/spec/update_spec.rb +40 -0
  47. 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