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.
Files changed (42) hide show
  1. data/MIT-LICENSE.txt +19 -0
  2. data/README.md +269 -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 +15 -0
  8. data/lib/core_ext/time.rb +11 -0
  9. data/lib/couch_potato.rb +41 -0
  10. data/lib/couch_potato/database.rb +96 -0
  11. data/lib/couch_potato/persistence.rb +94 -0
  12. data/lib/couch_potato/persistence/belongs_to_property.rb +58 -0
  13. data/lib/couch_potato/persistence/callbacks.rb +96 -0
  14. data/lib/couch_potato/persistence/dirty_attributes.rb +27 -0
  15. data/lib/couch_potato/persistence/json.rb +45 -0
  16. data/lib/couch_potato/persistence/magic_timestamps.rb +13 -0
  17. data/lib/couch_potato/persistence/properties.rb +56 -0
  18. data/lib/couch_potato/persistence/simple_property.rb +77 -0
  19. data/lib/couch_potato/view/base_view_spec.rb +24 -0
  20. data/lib/couch_potato/view/custom_view_spec.rb +27 -0
  21. data/lib/couch_potato/view/custom_views.rb +44 -0
  22. data/lib/couch_potato/view/model_view_spec.rb +63 -0
  23. data/lib/couch_potato/view/properties_view_spec.rb +39 -0
  24. data/lib/couch_potato/view/raw_view_spec.rb +25 -0
  25. data/lib/couch_potato/view/view_query.rb +44 -0
  26. data/rails/init.rb +7 -0
  27. data/spec/callbacks_spec.rb +271 -0
  28. data/spec/create_spec.rb +22 -0
  29. data/spec/custom_view_spec.rb +134 -0
  30. data/spec/destroy_spec.rb +29 -0
  31. data/spec/property_spec.rb +64 -0
  32. data/spec/spec.opts +4 -0
  33. data/spec/spec_helper.rb +31 -0
  34. data/spec/unit/attributes_spec.rb +26 -0
  35. data/spec/unit/create_spec.rb +58 -0
  36. data/spec/unit/customs_views_spec.rb +15 -0
  37. data/spec/unit/database_spec.rb +18 -0
  38. data/spec/unit/dirty_attributes_spec.rb +113 -0
  39. data/spec/unit/string_spec.rb +13 -0
  40. data/spec/unit/view_query_spec.rb +9 -0
  41. data/spec/update_spec.rb +40 -0
  42. 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