davber_couch_potato 0.3.0

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 (80) hide show
  1. data/CHANGES.md +106 -0
  2. data/MIT-LICENSE.txt +19 -0
  3. data/README.md +409 -0
  4. data/VERSION.yml +5 -0
  5. data/init.rb +3 -0
  6. data/lib/core_ext/date.rb +21 -0
  7. data/lib/core_ext/object.rb +5 -0
  8. data/lib/core_ext/string.rb +8 -0
  9. data/lib/core_ext/symbol.rb +15 -0
  10. data/lib/core_ext/time.rb +21 -0
  11. data/lib/couch_potato/database.rb +161 -0
  12. data/lib/couch_potato/persistence/active_model_compliance.rb +44 -0
  13. data/lib/couch_potato/persistence/attachments.rb +31 -0
  14. data/lib/couch_potato/persistence/callbacks.rb +62 -0
  15. data/lib/couch_potato/persistence/dirty_attributes.rb +56 -0
  16. data/lib/couch_potato/persistence/ghost_attributes.rb +22 -0
  17. data/lib/couch_potato/persistence/json.rb +46 -0
  18. data/lib/couch_potato/persistence/magic_timestamps.rb +20 -0
  19. data/lib/couch_potato/persistence/properties.rb +86 -0
  20. data/lib/couch_potato/persistence/simple_property.rb +72 -0
  21. data/lib/couch_potato/persistence/type_caster.rb +40 -0
  22. data/lib/couch_potato/persistence.rb +105 -0
  23. data/lib/couch_potato/railtie.rb +18 -0
  24. data/lib/couch_potato/rspec/matchers/json2.js +482 -0
  25. data/lib/couch_potato/rspec/matchers/list_as_matcher.rb +54 -0
  26. data/lib/couch_potato/rspec/matchers/map_to_matcher.rb +49 -0
  27. data/lib/couch_potato/rspec/matchers/print_r.js +60 -0
  28. data/lib/couch_potato/rspec/matchers/reduce_to_matcher.rb +50 -0
  29. data/lib/couch_potato/rspec/matchers.rb +39 -0
  30. data/lib/couch_potato/rspec/stub_db.rb +46 -0
  31. data/lib/couch_potato/rspec.rb +2 -0
  32. data/lib/couch_potato/validation/with_active_model.rb +27 -0
  33. data/lib/couch_potato/validation/with_validatable.rb +37 -0
  34. data/lib/couch_potato/validation.rb +16 -0
  35. data/lib/couch_potato/view/base_view_spec.rb +67 -0
  36. data/lib/couch_potato/view/custom_view_spec.rb +42 -0
  37. data/lib/couch_potato/view/custom_views.rb +52 -0
  38. data/lib/couch_potato/view/lists.rb +23 -0
  39. data/lib/couch_potato/view/model_view_spec.rb +75 -0
  40. data/lib/couch_potato/view/properties_view_spec.rb +47 -0
  41. data/lib/couch_potato/view/raw_view_spec.rb +25 -0
  42. data/lib/couch_potato/view/view_query.rb +78 -0
  43. data/lib/couch_potato.rb +79 -0
  44. data/rails/init.rb +4 -0
  45. data/rails/reload_classes.rb +47 -0
  46. data/spec/attachments_spec.rb +23 -0
  47. data/spec/callbacks_spec.rb +308 -0
  48. data/spec/create_spec.rb +34 -0
  49. data/spec/custom_view_spec.rb +239 -0
  50. data/spec/default_property_spec.rb +38 -0
  51. data/spec/destroy_spec.rb +29 -0
  52. data/spec/fixtures/address.rb +10 -0
  53. data/spec/fixtures/person.rb +6 -0
  54. data/spec/property_spec.rb +315 -0
  55. data/spec/rails_spec.rb +51 -0
  56. data/spec/spec.opts +4 -0
  57. data/spec/spec_helper.rb +47 -0
  58. data/spec/unit/active_model_compliance_spec.rb +98 -0
  59. data/spec/unit/attributes_spec.rb +125 -0
  60. data/spec/unit/base_view_spec_spec.rb +73 -0
  61. data/spec/unit/callbacks_spec.rb +72 -0
  62. data/spec/unit/couch_potato_spec.rb +39 -0
  63. data/spec/unit/create_spec.rb +58 -0
  64. data/spec/unit/custom_views_spec.rb +15 -0
  65. data/spec/unit/database_spec.rb +266 -0
  66. data/spec/unit/date_spec.rb +22 -0
  67. data/spec/unit/dirty_attributes_spec.rb +166 -0
  68. data/spec/unit/json_create_id_spec.rb +14 -0
  69. data/spec/unit/lists_spec.rb +20 -0
  70. data/spec/unit/model_view_spec_spec.rb +13 -0
  71. data/spec/unit/properties_view_spec_spec.rb +31 -0
  72. data/spec/unit/rspec_matchers_spec.rb +124 -0
  73. data/spec/unit/rspec_stub_db_spec.rb +35 -0
  74. data/spec/unit/string_spec.rb +7 -0
  75. data/spec/unit/time_spec.rb +22 -0
  76. data/spec/unit/validation_spec.rb +67 -0
  77. data/spec/unit/view_query_spec.rb +78 -0
  78. data/spec/update_spec.rb +40 -0
  79. data/spec/view_updates_spec.rb +28 -0
  80. metadata +205 -0
@@ -0,0 +1,161 @@
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
+ # executes a view and return the results. you pass in a view spec
16
+ # which is usually a result of a SomePersistentClass.some_view call.
17
+ # also return the total_rows returned by CouchDB as an accessor on the results.
18
+ #
19
+ # Example:
20
+ #
21
+ # class User
22
+ # include CouchPotato::Persistence
23
+ # property :age
24
+ # view :all, key: :age
25
+ # end
26
+ # db = CouchPotato.database
27
+ #
28
+ # db.view(User.all) # => [user1, user2]
29
+ # db.view(User.all).total_rows # => 2
30
+ #
31
+ # You can pass the usual parameters you can pass to a couchdb view to the view:
32
+ #
33
+ # db.view(User.all(limit: 5, startkey: 2, reduce: false))
34
+ #
35
+ # For your convenience when passing a hash with only a key parameter you can just pass in the value
36
+ #
37
+ # db.view(User.all(key: 1)) == db.view(User.all(1))
38
+ #
39
+ # Instead of passing a startkey and endkey you can pass in a key with a range:
40
+ #
41
+ # db.view(User.all(key: 1..20)) == db.view(startkey: 1, endkey: 20) == db.view(User.all(1..20))
42
+ #
43
+ # You can also pass in multiple keys:
44
+ #
45
+ # db.view(User.all(keys: [1, 2, 3]))
46
+ def view(spec)
47
+ results = CouchPotato::View::ViewQuery.new(
48
+ database,
49
+ spec.design_document,
50
+ {spec.view_name => {
51
+ :map => spec.map_function,
52
+ :reduce => spec.reduce_function}
53
+ },
54
+ ({spec.list_name => spec.list_function} unless spec.list_name.nil?)
55
+ ).query_view!(spec.view_parameters)
56
+ processed_results = spec.process_results results
57
+ processed_results.instance_eval "def total_rows; #{results['total_rows']}; end" if results['total_rows']
58
+ processed_results.each do |document|
59
+ document.database = self if document.respond_to?(:database=)
60
+ end if processed_results.respond_to?(:each)
61
+ processed_results
62
+ end
63
+
64
+ # saves a document. returns true on success, false on failure
65
+ def save_document(document, validate = true)
66
+ return true unless document.dirty?
67
+ if document.new?
68
+ create_document(document, validate)
69
+ else
70
+ update_document(document, validate)
71
+ end
72
+ end
73
+ alias_method :save, :save_document
74
+
75
+ # saves a document, raises a CouchPotato::Database::ValidationsFailedError on failure
76
+ def save_document!(document)
77
+ save_document(document) || raise(ValidationsFailedError.new(document.errors.full_messages))
78
+ end
79
+ alias_method :save!, :save_document!
80
+
81
+ def destroy_document(document)
82
+ document.run_callbacks :before_destroy
83
+ document._deleted = true
84
+ database.delete_doc document.to_hash
85
+ document.run_callbacks :after_destroy
86
+ document._id = nil
87
+ document._rev = nil
88
+ end
89
+ alias_method :destroy, :destroy_document
90
+
91
+ # loads a document by its id
92
+ def load_document(id)
93
+ raise "Can't load a document without an id (got nil)" if id.nil?
94
+ begin
95
+ instance = database.get(id)
96
+ instance.database = self
97
+ instance
98
+ rescue(RestClient::ResourceNotFound)
99
+ nil
100
+ end
101
+ end
102
+ alias_method :load, :load_document
103
+
104
+ def inspect #:nodoc:
105
+ "#<CouchPotato::Database>"
106
+ end
107
+
108
+ private
109
+
110
+ def create_document(document, validate)
111
+ document.database = self
112
+
113
+ if validate
114
+ document.errors.clear
115
+ document.run_callbacks :before_validation_on_save
116
+ document.run_callbacks :before_validation_on_create
117
+ return false unless valid_document?(document)
118
+ end
119
+
120
+ document.run_callbacks :before_save
121
+ document.run_callbacks :before_create
122
+ res = database.save_doc document.to_hash
123
+ document._rev = res['rev']
124
+ document._id = res['id']
125
+ document.run_callbacks :after_save
126
+ document.run_callbacks :after_create
127
+ true
128
+ end
129
+
130
+ def update_document(document, validate)
131
+ if validate
132
+ document.errors.clear
133
+ document.run_callbacks :before_validation_on_save
134
+ document.run_callbacks :before_validation_on_update
135
+ return false unless valid_document?(document)
136
+ end
137
+
138
+ document.run_callbacks :before_save
139
+ document.run_callbacks :before_update
140
+ res = database.save_doc document.to_hash
141
+ document._rev = res['rev']
142
+ document.run_callbacks :after_save
143
+ document.run_callbacks :after_update
144
+ true
145
+ end
146
+
147
+ def valid_document?(document)
148
+ errors = document.errors.errors.dup
149
+ document.valid?
150
+ errors.each_pair do |k, v|
151
+ v.each {|message| document.errors.add(k, message)}
152
+ end
153
+ document.errors.empty?
154
+ end
155
+
156
+ def database
157
+ @database
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,44 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ module ActiveModelCompliance
4
+ begin
5
+ require 'active_model'
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ def to_model
12
+ self
13
+ end
14
+
15
+ def errors
16
+ super || []
17
+ end
18
+
19
+ def persisted?
20
+ !self.new?
21
+ end
22
+
23
+ def to_key
24
+ persisted? ? [to_param] : nil
25
+ end
26
+
27
+ def destroyed?
28
+ !!_deleted
29
+ end
30
+
31
+ module ClassMethods
32
+ def model_name
33
+ @model_name ||= ::ActiveModel::Name.new(self)
34
+ end
35
+ end
36
+
37
+ rescue LoadError, NameError
38
+ # if it's not installed you probably don't want to use it anyway
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+
@@ -0,0 +1,31 @@
1
+ module CouchPotato
2
+ module Attachments
3
+ def self.included(base) #:nodoc:
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,62 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ module Callbacks
4
+ def self.included(base) #:nodoc:
5
+ base.extend ClassMethods
6
+
7
+ base.class_eval do
8
+ attr_accessor :skip_callbacks
9
+ def self.callbacks #:nodoc:
10
+ @callbacks ||= {:before_validation => [], :before_validation_on_create => [],
11
+ :before_validation_on_update => [], :before_validation_on_save => [], :before_create => [],
12
+ :after_create => [], :before_update => [], :after_update => [],
13
+ :before_save => [], :after_save => [],
14
+ :before_destroy => [], :after_destroy => []}
15
+ end
16
+ end
17
+ end
18
+
19
+ # Runs all callbacks on a model with the given name, e.g. :after_create.
20
+ #
21
+ # This method is called by the CouchPotato::Database object when saving/destroying an object
22
+ def run_callbacks(name)
23
+ return if skip_callbacks
24
+
25
+ callbacks = self.class.ancestors.map do |clazz|
26
+ clazz.callbacks[name] if clazz.respond_to?(:callbacks)
27
+ end.flatten.compact.uniq
28
+
29
+ callbacks.each do |callback|
30
+ if [Symbol, String].include?(callback.class)
31
+ send callback
32
+ elsif callback.is_a?(Proc)
33
+ callback.call self
34
+ else
35
+ raise "Don't know how to handle callback of type #{callback.class.name}"
36
+ end
37
+ end
38
+ end
39
+
40
+ module ClassMethods
41
+ [
42
+ :before_validation,
43
+ :before_validation_on_create,
44
+ :before_validation_on_update,
45
+ :before_validation_on_save,
46
+ :before_create,
47
+ :before_save,
48
+ :before_update,
49
+ :before_destroy,
50
+ :after_update,
51
+ :after_save,
52
+ :after_create,
53
+ :after_destroy
54
+ ].each do |callback|
55
+ define_method callback do |*names|
56
+ callbacks[callback].push *names
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,56 @@
1
+ require 'bigdecimal'
2
+ module CouchPotato
3
+ module Persistence
4
+ module DirtyAttributes
5
+
6
+ def self.included(base) #:nodoc:
7
+ base.class_eval do
8
+ after_save :reset_dirty_attributes
9
+
10
+ def initialize(attributes = {})
11
+ super
12
+ assign_attribute_copies_for_dirty_tracking
13
+ end
14
+ end
15
+ end
16
+
17
+ # returns true if a model has dirty attributes, i.e. their value has changed since the last save
18
+ def dirty?
19
+ new? || @forced_dirty || self.class.properties.inject(false) do |res, property|
20
+ res || property.dirty?(self)
21
+ end
22
+ end
23
+
24
+ # marks a model as dirty
25
+ def is_dirty
26
+ @forced_dirty = true
27
+ end
28
+
29
+ private
30
+
31
+ def assign_attribute_copies_for_dirty_tracking
32
+ attributes.each do |name, value|
33
+ self.instance_variable_set("@#{name}_was", clone_attribute(value))
34
+ end if attributes
35
+ end
36
+
37
+ def reset_dirty_attributes
38
+ @forced_dirty = nil
39
+ self.class.properties.each do |property|
40
+ instance_variable_set("@#{property.name}_was", clone_attribute(send(property.name)))
41
+ end
42
+ end
43
+
44
+ def clone_attribute(value)
45
+ if [Fixnum, Symbol, TrueClass, FalseClass, NilClass, Float, BigDecimal].include?(value.class)
46
+ value
47
+ elsif [Hash, Array].include?(value.class)
48
+ #Deep clone
49
+ Marshal::load(Marshal::dump(value))
50
+ else
51
+ value.clone
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,22 @@
1
+ module CouchPotato
2
+ module GhostAttributes #:nodoc:
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) #:nodoc:
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(JSON.create_id => 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,20 @@
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|
9
+ model.created_at ||= Time.now
10
+ model.created_at_not_changed
11
+ model.updated_at ||= Time.now
12
+ model.updated_at_not_changed
13
+ }
14
+ before_update lambda {|model|
15
+ model.updated_at = Time.now
16
+ model.updated_at_not_changed}
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,86 @@
1
+ require File.dirname(__FILE__) + '/simple_property'
2
+
3
+ module CouchPotato
4
+ module Persistence
5
+ module Properties
6
+ class PropertyList
7
+ include Enumerable
8
+
9
+ attr_accessor :list
10
+
11
+ def initialize(clazz)
12
+ @clazz = clazz
13
+ @list = []
14
+ end
15
+
16
+ def each
17
+ (list + inherited_properties).each {|property| yield property}
18
+ end
19
+
20
+ def <<(property)
21
+ @list << property
22
+ end
23
+
24
+ def inherited_properties
25
+ superclazz = @clazz.superclass
26
+ properties = []
27
+ while superclazz && superclazz.respond_to?(:properties)
28
+ properties << superclazz.properties.list
29
+ superclazz = superclazz.superclass
30
+ end
31
+ properties.flatten
32
+ end
33
+ end
34
+
35
+ def self.included(base) #:nodoc:
36
+ base.extend ClassMethods
37
+ base.class_eval do
38
+ def self.properties
39
+ @properties ||= {}
40
+ @properties[name] ||= PropertyList.new(self)
41
+ end
42
+ end
43
+ end
44
+
45
+ def type_caster #:nodoc:
46
+ @type_caster ||= TypeCaster.new
47
+ end
48
+
49
+ module ClassMethods
50
+ # returns all the property names of a model class that have been defined using the #property method
51
+ #
52
+ # example:
53
+ # class Book
54
+ # property :title
55
+ # property :year
56
+ # end
57
+ # Book.property_names # => [:title, :year]
58
+ def property_names
59
+ properties.map(&:name)
60
+ end
61
+
62
+ def json_create(json) #:nodoc:
63
+ return if json.nil?
64
+ instance = super
65
+ instance.send(:assign_attribute_copies_for_dirty_tracking)
66
+ instance
67
+ end
68
+
69
+ # Declare a property on a model class. Properties are not typed by default.
70
+ # You can store anything in a property that can be serialized into JSON.
71
+ # If you want a property to be of a custom class you have to define it using the :type option.
72
+ #
73
+ # example:
74
+ # class Book
75
+ # property :title
76
+ # property :year
77
+ # property :publisher, :type => Publisher
78
+ # end
79
+ def property(name, options = {})
80
+ properties << SimpleProperty.new(self, name, options)
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,72 @@
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
+ @type_caster = TypeCaster.new
10
+
11
+ define_accessors accessors_module_for(owner_clazz), name, options
12
+ end
13
+
14
+ def build(object, json)
15
+ value = json[name.to_s].nil? ? json[name.to_sym] : json[name.to_s]
16
+ object.send "#{name}=", value
17
+ end
18
+
19
+ def dirty?(object)
20
+ object.send("#{name}_changed?")
21
+ end
22
+
23
+ def serialize(json, object)
24
+ json[name] = object.send name
25
+ end
26
+ alias :value :serialize
27
+
28
+ private
29
+
30
+ def accessors_module_for(clazz)
31
+ unless clazz.const_defined?('AccessorMethods')
32
+ accessors_module = clazz.const_set('AccessorMethods', Module.new)
33
+ clazz.send(:include, accessors_module)
34
+ end
35
+ clazz.const_get('AccessorMethods')
36
+ end
37
+
38
+ def define_accessors(base, name, options)
39
+ base.class_eval do
40
+ attr_reader "#{name}_was"
41
+
42
+ define_method "#{name}" do
43
+ value = self.instance_variable_get("@#{name}")
44
+ if value.nil? && options[:default]
45
+ default = clone_attribute(options[:default])
46
+ self.instance_variable_set("@#{name}", default)
47
+ default
48
+ else
49
+ value
50
+ end
51
+ end
52
+
53
+ define_method "#{name}=" do |value|
54
+ self.instance_variable_set("@#{name}", type_caster.cast(value, options[:type]))
55
+ end
56
+
57
+ define_method "#{name}?" do
58
+ !self.send(name).nil? && !self.send(name).try(:blank?)
59
+ end
60
+
61
+ define_method "#{name}_changed?" do
62
+ !self.instance_variable_get("@#{name}_not_changed") && self.send(name) != self.send("#{name}_was")
63
+ end
64
+
65
+ define_method "#{name}_not_changed" do
66
+ self.instance_variable_set("@#{name}_not_changed", true)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,40 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ class TypeCaster #:nodoc:
4
+ def cast(value, type)
5
+ if type == :boolean
6
+ cast_boolen(value)
7
+ else
8
+ cast_native(value, type)
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def cast_boolen(value)
15
+ if [FalseClass, TrueClass].include?(value.class) || value.nil?
16
+ value
17
+ elsif [0, '0'].include?(value)
18
+ false
19
+ else
20
+ true
21
+ end
22
+ end
23
+
24
+ def cast_native(value, type)
25
+ if type && !value.instance_of?(type)
26
+ if type == Fixnum
27
+ value.to_s.scan(/\d/).join.to_i unless value.blank?
28
+ elsif type == Float
29
+ value.to_s.scan(/\d+\.?\d*/).join.to_f unless value.blank?
30
+ else
31
+ type.json_create value unless value.blank?
32
+ end
33
+ else
34
+ value
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end