couch_potato-rails2 0.5.6
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/.gitignore +7 -0
- data/.travis.yml +5 -0
- data/CHANGES.md +148 -0
- data/CREDITS +6 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.md +450 -0
- data/Rakefile +82 -0
- data/couch_potato.gemspec +27 -0
- data/init.rb +3 -0
- data/lib/core_ext/date.rb +14 -0
- data/lib/core_ext/object.rb +5 -0
- data/lib/core_ext/string.rb +12 -0
- data/lib/core_ext/symbol.rb +15 -0
- data/lib/core_ext/time.rb +23 -0
- data/lib/couch_potato.rb +48 -0
- data/lib/couch_potato/database.rb +179 -0
- data/lib/couch_potato/persistence.rb +124 -0
- data/lib/couch_potato/persistence/active_model_compliance.rb +44 -0
- data/lib/couch_potato/persistence/attachments.rb +31 -0
- data/lib/couch_potato/persistence/callbacks.rb +29 -0
- data/lib/couch_potato/persistence/dirty_attributes.rb +56 -0
- data/lib/couch_potato/persistence/ghost_attributes.rb +12 -0
- data/lib/couch_potato/persistence/json.rb +47 -0
- data/lib/couch_potato/persistence/magic_timestamps.rb +23 -0
- data/lib/couch_potato/persistence/properties.rb +79 -0
- data/lib/couch_potato/persistence/simple_property.rb +82 -0
- data/lib/couch_potato/persistence/type_caster.rb +40 -0
- data/lib/couch_potato/railtie.rb +25 -0
- data/lib/couch_potato/rspec.rb +2 -0
- data/lib/couch_potato/rspec/matchers.rb +39 -0
- data/lib/couch_potato/rspec/matchers/json2.js +482 -0
- data/lib/couch_potato/rspec/matchers/list_as_matcher.rb +54 -0
- data/lib/couch_potato/rspec/matchers/map_to_matcher.rb +49 -0
- data/lib/couch_potato/rspec/matchers/print_r.js +60 -0
- data/lib/couch_potato/rspec/matchers/reduce_to_matcher.rb +50 -0
- data/lib/couch_potato/rspec/stub_db.rb +46 -0
- data/lib/couch_potato/validation.rb +16 -0
- data/lib/couch_potato/validation/with_active_model.rb +27 -0
- data/lib/couch_potato/validation/with_validatable.rb +41 -0
- data/lib/couch_potato/version.rb +3 -0
- data/lib/couch_potato/view/base_view_spec.rb +84 -0
- data/lib/couch_potato/view/custom_view_spec.rb +42 -0
- data/lib/couch_potato/view/custom_views.rb +52 -0
- data/lib/couch_potato/view/lists.rb +23 -0
- data/lib/couch_potato/view/model_view_spec.rb +75 -0
- data/lib/couch_potato/view/properties_view_spec.rb +47 -0
- data/lib/couch_potato/view/raw_view_spec.rb +25 -0
- data/lib/couch_potato/view/view_query.rb +82 -0
- data/rails/init.rb +4 -0
- data/rails/reload_classes.rb +47 -0
- data/spec/attachments_spec.rb +23 -0
- data/spec/callbacks_spec.rb +297 -0
- data/spec/create_spec.rb +35 -0
- data/spec/custom_view_spec.rb +239 -0
- data/spec/default_property_spec.rb +38 -0
- data/spec/destroy_spec.rb +29 -0
- data/spec/fixtures/address.rb +10 -0
- data/spec/fixtures/person.rb +6 -0
- data/spec/property_spec.rb +323 -0
- data/spec/rails_spec.rb +50 -0
- data/spec/railtie_spec.rb +65 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/unit/active_model_compliance_spec.rb +98 -0
- data/spec/unit/attributes_spec.rb +135 -0
- data/spec/unit/base_view_spec_spec.rb +106 -0
- data/spec/unit/callbacks_spec.rb +46 -0
- data/spec/unit/couch_potato_spec.rb +39 -0
- data/spec/unit/create_spec.rb +69 -0
- data/spec/unit/custom_views_spec.rb +15 -0
- data/spec/unit/database_spec.rb +317 -0
- data/spec/unit/date_spec.rb +22 -0
- data/spec/unit/dirty_attributes_spec.rb +136 -0
- data/spec/unit/initialize_spec.rb +38 -0
- data/spec/unit/json_spec.rb +30 -0
- data/spec/unit/lists_spec.rb +20 -0
- data/spec/unit/model_view_spec_spec.rb +13 -0
- data/spec/unit/properties_view_spec_spec.rb +31 -0
- data/spec/unit/rspec_matchers_spec.rb +124 -0
- data/spec/unit/rspec_stub_db_spec.rb +35 -0
- data/spec/unit/string_spec.rb +7 -0
- data/spec/unit/time_spec.rb +15 -0
- data/spec/unit/validation_spec.rb +67 -0
- data/spec/unit/view_query_spec.rb +86 -0
- data/spec/update_spec.rb +40 -0
- data/spec/view_updates_spec.rb +28 -0
- metadata +243 -0
@@ -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 && _attachments.any?
|
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,29 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_model/callbacks'
|
3
|
+
|
4
|
+
module CouchPotato
|
5
|
+
module Persistence
|
6
|
+
module Callbacks
|
7
|
+
def self.included(base) #:nodoc:
|
8
|
+
base.extend ActiveModel::Callbacks
|
9
|
+
|
10
|
+
base.class_eval do
|
11
|
+
attr_accessor :skip_callbacks
|
12
|
+
|
13
|
+
define_model_callbacks :create, :save, :update, :destroy
|
14
|
+
define_model_callbacks *[:save, :create, :update].map {|c| :"validation_on_#{c}"}
|
15
|
+
define_model_callbacks :validation unless Config.validation_framework == :active_model
|
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, &block)
|
23
|
+
return if skip_callbacks
|
24
|
+
|
25
|
+
send(:"_run_#{name}_callbacks", &block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
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.send :include, ActiveModel::Dirty
|
8
|
+
base.class_eval do
|
9
|
+
after_save :reset_dirty_attributes
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(attributes = {})
|
14
|
+
super
|
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
|
+
changed? || @forced_dirty
|
20
|
+
end
|
21
|
+
|
22
|
+
# marks a model as dirty
|
23
|
+
def is_dirty
|
24
|
+
@forced_dirty = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(name, *args)
|
28
|
+
if(name.to_s.include?('_will_change!'))
|
29
|
+
self.class.define_attribute_methods self.class.property_names
|
30
|
+
send(name, *args)
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def reset_dirty_attributes
|
39
|
+
@previously_changed = changes
|
40
|
+
@changed_attributes.clear
|
41
|
+
@forced_dirty = nil
|
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,47 @@
|
|
1
|
+
module CouchPotato
|
2
|
+
module Persistence
|
3
|
+
module Json
|
4
|
+
def self.included(base) #:nodoc:
|
5
|
+
base.class_eval do
|
6
|
+
extend ClassMethods
|
7
|
+
attr_accessor :_document
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# returns a JSON representation of a model in order to store it in CouchDB
|
12
|
+
def to_json(*args)
|
13
|
+
to_hash.to_json(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
# returns all the attributes, the ruby class and the _id and _rev of a model as a Hash
|
17
|
+
def to_hash
|
18
|
+
(self.class.properties).inject({}) do |props, property|
|
19
|
+
property.serialize(props, self)
|
20
|
+
props
|
21
|
+
end.merge(JSON.create_id => self.class.name).merge(id_and_rev_json)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def id_and_rev_json
|
27
|
+
['_id', '_rev', '_deleted'].inject({}) do |hash, key|
|
28
|
+
hash[key] = self.send(key) unless self.send(key).nil?
|
29
|
+
hash
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
|
35
|
+
# creates a model instance from JSON
|
36
|
+
def json_create(json)
|
37
|
+
return if json.nil?
|
38
|
+
instance = self.new
|
39
|
+
instance._id = json[:_id] || json['_id']
|
40
|
+
instance._rev = json[:_rev] || json['_rev']
|
41
|
+
instance._document = HashWithIndifferentAccess.new(json)
|
42
|
+
instance
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'active_support/time'
|
2
|
+
|
3
|
+
module CouchPotato
|
4
|
+
module MagicTimestamps #:nodoc:
|
5
|
+
def self.included(base)
|
6
|
+
base.instance_eval do
|
7
|
+
property :created_at, :type => Time
|
8
|
+
property :updated_at, :type => Time
|
9
|
+
|
10
|
+
before_create lambda {|model|
|
11
|
+
model.created_at ||= (Time.zone || Time).now
|
12
|
+
@changed_attributes.delete 'created_at'
|
13
|
+
model.updated_at ||= (Time.zone || Time).now
|
14
|
+
@changed_attributes.delete 'updated_at'
|
15
|
+
}
|
16
|
+
before_update lambda {|model|
|
17
|
+
model.updated_at = (Time.zone || Time).now
|
18
|
+
@changed_attributes.delete 'updated_at'
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,79 @@
|
|
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
|
+
# Declare a property on a model class. Properties are not typed by default.
|
63
|
+
# You can store anything in a property that can be serialized into JSON.
|
64
|
+
# If you want a property to be of a custom class you have to define it using the :type option.
|
65
|
+
#
|
66
|
+
# example:
|
67
|
+
# class Book
|
68
|
+
# property :title
|
69
|
+
# property :year
|
70
|
+
# property :publisher, :type => Publisher
|
71
|
+
# end
|
72
|
+
def property(name, options = {})
|
73
|
+
properties << SimpleProperty.new(self, name, options)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module CouchPotato
|
2
|
+
module Persistence
|
3
|
+
module PropertyMethods
|
4
|
+
private
|
5
|
+
|
6
|
+
def load_attribute_from_document(name)
|
7
|
+
if (_document || {}).has_key?(name.to_s)
|
8
|
+
property = self.class.properties.find{|property| property.name == name}
|
9
|
+
@skip_dirty_tracking = true
|
10
|
+
value = property.build(self, _document)
|
11
|
+
@skip_dirty_tracking = false
|
12
|
+
value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class SimpleProperty #:nodoc:
|
18
|
+
attr_accessor :name, :type
|
19
|
+
|
20
|
+
def initialize(owner_clazz, name, options = {})
|
21
|
+
self.name = name
|
22
|
+
self.type = options[:type]
|
23
|
+
@type_caster = TypeCaster.new
|
24
|
+
owner_clazz.send :include, PropertyMethods unless owner_clazz.ancestors.include?(PropertyMethods)
|
25
|
+
|
26
|
+
define_accessors accessors_module_for(owner_clazz), name, options
|
27
|
+
end
|
28
|
+
|
29
|
+
def build(object, json)
|
30
|
+
value = json[name.to_s].nil? ? json[name.to_sym] : json[name.to_s]
|
31
|
+
object.send "#{name}=", value
|
32
|
+
end
|
33
|
+
|
34
|
+
def dirty?(object)
|
35
|
+
object.send("#{name}_changed?")
|
36
|
+
end
|
37
|
+
|
38
|
+
def serialize(json, object)
|
39
|
+
json[name] = object.send name
|
40
|
+
end
|
41
|
+
alias :value :serialize
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def accessors_module_for(clazz)
|
46
|
+
unless clazz.const_defined?('AccessorMethods')
|
47
|
+
accessors_module = clazz.const_set('AccessorMethods', Module.new)
|
48
|
+
clazz.send(:include, accessors_module)
|
49
|
+
end
|
50
|
+
clazz.const_get('AccessorMethods')
|
51
|
+
end
|
52
|
+
|
53
|
+
def define_accessors(base, name, options)
|
54
|
+
base.class_eval do
|
55
|
+
include PropertyMethods
|
56
|
+
|
57
|
+
define_method "#{name}" do
|
58
|
+
load_attribute_from_document(name) unless instance_variable_defined?("@#{name}")
|
59
|
+
value = instance_variable_get("@#{name}")
|
60
|
+
if value.nil? && options[:default]
|
61
|
+
default = clone_attribute(options[:default])
|
62
|
+
self.instance_variable_set("@#{name}", default)
|
63
|
+
default
|
64
|
+
else
|
65
|
+
value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
define_method "#{name}=" do |value|
|
70
|
+
typecasted_value = type_caster.cast(value, options[:type])
|
71
|
+
send("#{name}_will_change!") unless @skip_dirty_tracking || typecasted_value == send(name)
|
72
|
+
self.instance_variable_set("@#{name}", typecasted_value)
|
73
|
+
end
|
74
|
+
|
75
|
+
define_method "#{name}?" do
|
76
|
+
!self.send(name).nil? && !self.send(name).try(:blank?)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
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_boolean(value)
|
7
|
+
else
|
8
|
+
cast_native(value, type)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def cast_boolean(value)
|
15
|
+
if [FalseClass, TrueClass].include?(value.class) || value.nil?
|
16
|
+
value
|
17
|
+
elsif [0, '0', 'false'].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
|