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.
- data/CHANGES.md +106 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.md +409 -0
- data/VERSION.yml +5 -0
- data/init.rb +3 -0
- data/lib/core_ext/date.rb +21 -0
- data/lib/core_ext/object.rb +5 -0
- data/lib/core_ext/string.rb +8 -0
- data/lib/core_ext/symbol.rb +15 -0
- data/lib/core_ext/time.rb +21 -0
- data/lib/couch_potato/database.rb +161 -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 +62 -0
- data/lib/couch_potato/persistence/dirty_attributes.rb +56 -0
- data/lib/couch_potato/persistence/ghost_attributes.rb +22 -0
- data/lib/couch_potato/persistence/json.rb +46 -0
- data/lib/couch_potato/persistence/magic_timestamps.rb +20 -0
- data/lib/couch_potato/persistence/properties.rb +86 -0
- data/lib/couch_potato/persistence/simple_property.rb +72 -0
- data/lib/couch_potato/persistence/type_caster.rb +40 -0
- data/lib/couch_potato/persistence.rb +105 -0
- data/lib/couch_potato/railtie.rb +18 -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/matchers.rb +39 -0
- data/lib/couch_potato/rspec/stub_db.rb +46 -0
- data/lib/couch_potato/rspec.rb +2 -0
- data/lib/couch_potato/validation/with_active_model.rb +27 -0
- data/lib/couch_potato/validation/with_validatable.rb +37 -0
- data/lib/couch_potato/validation.rb +16 -0
- data/lib/couch_potato/view/base_view_spec.rb +67 -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 +78 -0
- data/lib/couch_potato.rb +79 -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 +308 -0
- data/spec/create_spec.rb +34 -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 +315 -0
- data/spec/rails_spec.rb +51 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/unit/active_model_compliance_spec.rb +98 -0
- data/spec/unit/attributes_spec.rb +125 -0
- data/spec/unit/base_view_spec_spec.rb +73 -0
- data/spec/unit/callbacks_spec.rb +72 -0
- data/spec/unit/couch_potato_spec.rb +39 -0
- data/spec/unit/create_spec.rb +58 -0
- data/spec/unit/custom_views_spec.rb +15 -0
- data/spec/unit/database_spec.rb +266 -0
- data/spec/unit/date_spec.rb +22 -0
- data/spec/unit/dirty_attributes_spec.rb +166 -0
- data/spec/unit/json_create_id_spec.rb +14 -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 +22 -0
- data/spec/unit/validation_spec.rb +67 -0
- data/spec/unit/view_query_spec.rb +78 -0
- data/spec/update_spec.rb +40 -0
- data/spec/view_updates_spec.rb +28 -0
- 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
|