couch_tomato 0.1.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/MIT-LICENSE.txt +19 -0
- data/README.md +96 -0
- data/init.rb +3 -0
- data/lib/core_ext/date.rb +10 -0
- data/lib/core_ext/duplicable.rb +43 -0
- data/lib/core_ext/extract_options.rb +14 -0
- data/lib/core_ext/inheritable_attributes.rb +222 -0
- data/lib/core_ext/object.rb +5 -0
- data/lib/core_ext/string.rb +19 -0
- data/lib/core_ext/symbol.rb +15 -0
- data/lib/core_ext/time.rb +12 -0
- data/lib/couch_tomato/database.rb +279 -0
- data/lib/couch_tomato/js_view_source.rb +182 -0
- data/lib/couch_tomato/migration.rb +52 -0
- data/lib/couch_tomato/migrator.rb +235 -0
- data/lib/couch_tomato/persistence/base.rb +62 -0
- data/lib/couch_tomato/persistence/belongs_to_property.rb +58 -0
- data/lib/couch_tomato/persistence/callbacks.rb +60 -0
- data/lib/couch_tomato/persistence/dirty_attributes.rb +27 -0
- data/lib/couch_tomato/persistence/json.rb +48 -0
- data/lib/couch_tomato/persistence/magic_timestamps.rb +15 -0
- data/lib/couch_tomato/persistence/properties.rb +58 -0
- data/lib/couch_tomato/persistence/simple_property.rb +97 -0
- data/lib/couch_tomato/persistence/validation.rb +18 -0
- data/lib/couch_tomato/persistence.rb +85 -0
- data/lib/couch_tomato/replicator.rb +50 -0
- data/lib/couch_tomato.rb +46 -0
- data/lib/tasks/couch_tomato.rake +128 -0
- data/rails/init.rb +7 -0
- data/spec/callbacks_spec.rb +271 -0
- data/spec/comment.rb +8 -0
- data/spec/create_spec.rb +22 -0
- data/spec/custom_view_spec.rb +134 -0
- data/spec/destroy_spec.rb +29 -0
- data/spec/fixtures/address.rb +9 -0
- data/spec/fixtures/person.rb +6 -0
- data/spec/property_spec.rb +103 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/unit/attributes_spec.rb +26 -0
- data/spec/unit/callbacks_spec.rb +33 -0
- data/spec/unit/create_spec.rb +58 -0
- data/spec/unit/customs_views_spec.rb +15 -0
- data/spec/unit/database_spec.rb +38 -0
- data/spec/unit/dirty_attributes_spec.rb +113 -0
- data/spec/unit/string_spec.rb +13 -0
- data/spec/unit/view_query_spec.rb +9 -0
- data/spec/update_spec.rb +40 -0
- data/test/test_helper.rb +63 -0
- data/test/unit/database_test.rb +285 -0
- data/test/unit/js_view_test.rb +362 -0
- data/test/unit/property_test.rb +193 -0
- metadata +133 -0
@@ -0,0 +1,235 @@
|
|
1
|
+
module CouchTomato
|
2
|
+
class IrreversibleMigration < StandardError#:nodoc:
|
3
|
+
end
|
4
|
+
|
5
|
+
class DuplicateMigrationVersionError < StandardError#:nodoc:
|
6
|
+
def initialize(version)
|
7
|
+
super("Multiple migrations have the version number #{version}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class DuplicateMigrationNameError < StandardError#:nodoc:
|
12
|
+
def initialize(name)
|
13
|
+
super("Multiple migrations have the name #{name}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class UnknownMigrationVersionError < StandardError #:nodoc:
|
18
|
+
def initialize(version)
|
19
|
+
super("No migration with version number #{version}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class IllegalMigrationNameError < StandardError#:nodoc:
|
24
|
+
def initialize(name)
|
25
|
+
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# MigrationProxy is used to defer loading of the actual migration classes
|
30
|
+
# until they are needed
|
31
|
+
class MigrationProxy
|
32
|
+
attr_accessor :name, :version, :filename
|
33
|
+
|
34
|
+
delegate :migrate, :announce, :write, :to=>:migration
|
35
|
+
|
36
|
+
private
|
37
|
+
def migration
|
38
|
+
@migration ||= load_migration
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_migration
|
42
|
+
load(filename)
|
43
|
+
name.constantize
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Migrator#:nodoc:
|
48
|
+
class << self
|
49
|
+
def migrate(db, migrations_path, target_version = nil)
|
50
|
+
case
|
51
|
+
when target_version.nil? then up(db, migrations_path, target_version)
|
52
|
+
when current_version(db) > target_version then down(db, migrations_path, target_version)
|
53
|
+
else up(db, migrations_path, target_version)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def rollback(db, migrations_path, steps=1)
|
58
|
+
move(:down, db, migrations_path, steps)
|
59
|
+
end
|
60
|
+
|
61
|
+
def forward(db, migrations_path, steps=1)
|
62
|
+
move(:up, db, migrations_path, steps)
|
63
|
+
end
|
64
|
+
|
65
|
+
def up(db, migrations_path, target_version = nil)
|
66
|
+
self.new(:up, db, migrations_path, target_version).migrate
|
67
|
+
end
|
68
|
+
|
69
|
+
def down(db, migrations_path, target_version = nil)
|
70
|
+
self.new(:down, db, migrations_path, target_version).migrate
|
71
|
+
end
|
72
|
+
|
73
|
+
def run(direction, db, migrations_path, target_version)
|
74
|
+
self.new(direction, db, migrations_path, target_version).run
|
75
|
+
end
|
76
|
+
|
77
|
+
def migrations_doc(db)
|
78
|
+
begin
|
79
|
+
doc = db.get('_design/migrations')
|
80
|
+
rescue RestClient::ResourceNotFound
|
81
|
+
db.save_doc('_id' => '_design/migrations', 'versions' => nil)
|
82
|
+
doc = db.get('_design/migrations')
|
83
|
+
end
|
84
|
+
|
85
|
+
return doc
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_all_versions(db)
|
89
|
+
doc = migrations_doc(db)
|
90
|
+
|
91
|
+
if doc['versions']
|
92
|
+
doc['versions'].sort
|
93
|
+
else
|
94
|
+
[]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def current_version(db)
|
99
|
+
get_all_versions(db).max || 0
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def move(direction, db, migrations_path, steps)
|
105
|
+
migrator = self.new(direction, db, migrations_path)
|
106
|
+
start_index = migrator.migrations.index(migrator.current_migration) || 0
|
107
|
+
|
108
|
+
finish = migrator.migrations[start_index + steps]
|
109
|
+
version = finish ? finish.version : 0
|
110
|
+
send(direction, db, migrations_path, version)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def initialize(direction, db, migrations_path, target_version = nil)
|
115
|
+
@db = db
|
116
|
+
@migrations_doc = self.class.migrations_doc(@db)
|
117
|
+
@direction, @migrations_path, @target_version = direction, migrations_path, target_version
|
118
|
+
end
|
119
|
+
|
120
|
+
def current_version
|
121
|
+
migrated.last || 0
|
122
|
+
end
|
123
|
+
|
124
|
+
def current_migration
|
125
|
+
migrations.detect { |m| m.version == current_version }
|
126
|
+
end
|
127
|
+
|
128
|
+
def run
|
129
|
+
target = migrations.detect { |m| m.version == @target_version }
|
130
|
+
raise UnknownMigrationVersionError.new(@target_version) if target.nil?
|
131
|
+
unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
|
132
|
+
target.migrate(@direction, @db)
|
133
|
+
record_version_state_after_migrating(target.version)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def migrate
|
138
|
+
current = migrations.detect { |m| m.version == current_version }
|
139
|
+
target = migrations.detect { |m| m.version == @target_version }
|
140
|
+
|
141
|
+
if target.nil? && !@target_version.nil? && @target_version > 0
|
142
|
+
raise UnknownMigrationVersionError.new(@target_version)
|
143
|
+
end
|
144
|
+
|
145
|
+
start = up? ? 0 : (migrations.index(current) || 0)
|
146
|
+
finish = migrations.index(target) || migrations.size - 1
|
147
|
+
runnable = migrations[start..finish]
|
148
|
+
|
149
|
+
# skip the last migration if we're headed down, but not ALL the way down
|
150
|
+
runnable.pop if down? && !target.nil?
|
151
|
+
|
152
|
+
runnable.each do |migration|
|
153
|
+
puts "Migrating to #{migration.name} (#{migration.version})"
|
154
|
+
|
155
|
+
# On our way up, we skip migrating the ones we've already migrated
|
156
|
+
next if up? && migrated.include?(migration.version.to_i)
|
157
|
+
|
158
|
+
# On our way down, we skip reverting the ones we've never migrated
|
159
|
+
if down? && !migrated.include?(migration.version.to_i)
|
160
|
+
migration.announce 'never migrated, skipping'; migration.write
|
161
|
+
next
|
162
|
+
end
|
163
|
+
|
164
|
+
begin
|
165
|
+
migration.migrate(@direction, @db)
|
166
|
+
record_version_state_after_migrating(migration.version)
|
167
|
+
rescue => e
|
168
|
+
raise StandardError, "An error has occurred, all later migrations canceled:\n\n#{e}", e.backtrace
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def migrations
|
174
|
+
@migrations ||= begin
|
175
|
+
files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
|
176
|
+
|
177
|
+
migrations = files.inject([]) do |klasses, file|
|
178
|
+
version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
|
179
|
+
|
180
|
+
raise IllegalMigrationNameError.new(file) unless version
|
181
|
+
version = version.to_i
|
182
|
+
|
183
|
+
if klasses.detect { |m| m.version == version }
|
184
|
+
raise DuplicateMigrationVersionError.new(version)
|
185
|
+
end
|
186
|
+
|
187
|
+
if klasses.detect { |m| m.name == name.camelize }
|
188
|
+
raise DuplicateMigrationNameError.new(name.camelize)
|
189
|
+
end
|
190
|
+
|
191
|
+
migration = MigrationProxy.new
|
192
|
+
migration.name = name.camelize
|
193
|
+
migration.version = version
|
194
|
+
migration.filename = file
|
195
|
+
klasses << migration
|
196
|
+
end
|
197
|
+
|
198
|
+
migrations = migrations.sort_by(&:version)
|
199
|
+
down? ? migrations.reverse : migrations
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def pending_migrations
|
204
|
+
already_migrated = migrated
|
205
|
+
migrations.reject { |m| already_migrated.include?(m.version.to_i) }
|
206
|
+
end
|
207
|
+
|
208
|
+
def migrated
|
209
|
+
@migrated_versions ||= self.class.get_all_versions(@db)
|
210
|
+
end
|
211
|
+
|
212
|
+
private
|
213
|
+
def record_version_state_after_migrating(version)
|
214
|
+
@migrated_versions ||= []
|
215
|
+
if down?
|
216
|
+
@migrated_versions.delete(version.to_i)
|
217
|
+
@migrations_doc['versions'].delete(version)
|
218
|
+
@db.save_doc(@migrations_doc)
|
219
|
+
else
|
220
|
+
@migrated_versions.push(version.to_i).sort!
|
221
|
+
@migrations_doc['versions'] ||= []
|
222
|
+
@migrations_doc['versions'].push(version)
|
223
|
+
@db.save_doc(@migrations_doc)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def up?
|
228
|
+
@direction == :up
|
229
|
+
end
|
230
|
+
|
231
|
+
def down?
|
232
|
+
@direction == :down
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module CouchTomato
|
2
|
+
module Persistence
|
3
|
+
module Base
|
4
|
+
# initialize a new instance of the model optionally passing it a hash of attributes.
|
5
|
+
# the attributes have to be declared using the #property method
|
6
|
+
#
|
7
|
+
# example:
|
8
|
+
# class Book
|
9
|
+
# include CouchTomato::Persistence
|
10
|
+
# property :title
|
11
|
+
# end
|
12
|
+
# book = Book.new :title => 'Time to Relax'
|
13
|
+
# book.title # => 'Time to Relax'
|
14
|
+
def initialize(attributes = {})
|
15
|
+
attributes.each do |name, value|
|
16
|
+
self.send("#{name}=", value)
|
17
|
+
end if attributes
|
18
|
+
end
|
19
|
+
|
20
|
+
# assign multiple attributes at once.
|
21
|
+
# the attributes have to be declared using the #property method
|
22
|
+
#
|
23
|
+
# example:
|
24
|
+
# class Book
|
25
|
+
# include CouchTomato::Persistence
|
26
|
+
# property :title
|
27
|
+
# property :year
|
28
|
+
# end
|
29
|
+
# book = Book.new
|
30
|
+
# book.attributes = {:title => 'Time to Relax', :year => 2009}
|
31
|
+
# book.title # => 'Time to Relax'
|
32
|
+
# book.year # => 2009
|
33
|
+
def attributes=(hash)
|
34
|
+
hash.each do |attribute, value|
|
35
|
+
self.send "#{attribute}=", value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# returns all of a model's attributes that have been defined using the #property method as a Hash
|
40
|
+
#
|
41
|
+
# example:
|
42
|
+
# class Book
|
43
|
+
# include CouchTomato::Persistence
|
44
|
+
# property :title
|
45
|
+
# property :year
|
46
|
+
# end
|
47
|
+
# book = Book.new :year => 2009
|
48
|
+
# book.attributes # => {:title => nil, :year => 2009}
|
49
|
+
def attributes
|
50
|
+
self.class.properties.inject({}) do |res, property|
|
51
|
+
property.serialize(res, self)
|
52
|
+
res
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def ==(other) #:nodoc:
|
57
|
+
other.class == self.class && self.to_json == other.to_json
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module CouchTomato
|
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 CouchTomato
|
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 CouchTomato::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 CouchTomato
|
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,48 @@
|
|
1
|
+
module CouchTomato
|
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
|
+
# to_json(*args)
|
12
|
+
end
|
13
|
+
|
14
|
+
# returns all the attributes, the ruby class and the _id and _rev of a model as a Hash
|
15
|
+
def to_hash
|
16
|
+
(self.class.properties).inject({}) do |props, property|
|
17
|
+
property.serialize(props, self)
|
18
|
+
props
|
19
|
+
end.merge('ruby_class' => self.class.name).merge(id_and_rev_json)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def id_and_rev_json
|
25
|
+
['_id', '_rev', '_deleted'].inject({}) do |hash, key|
|
26
|
+
hash[key] = self.send(key) unless self.send(key).nil?
|
27
|
+
hash
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
|
33
|
+
# creates a model instance from JSON
|
34
|
+
def json_create(json, meta={})
|
35
|
+
return if json.nil?
|
36
|
+
instance = self.new
|
37
|
+
instance._id = json[:_id] || json['_id']
|
38
|
+
instance._rev = json[:_rev] || json['_rev']
|
39
|
+
properties.each do |property|
|
40
|
+
property.build(instance, json)
|
41
|
+
end
|
42
|
+
instance.metadata = meta
|
43
|
+
instance
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module CouchTomato
|
2
|
+
module Persistence
|
3
|
+
module MagicTimestamps #:nodoc:
|
4
|
+
def self.included(base)
|
5
|
+
base.instance_eval do
|
6
|
+
property :created_at, :type => Time
|
7
|
+
property :updated_at, :type => Time
|
8
|
+
|
9
|
+
before_create lambda {|model| model.created_at = Time.now; model.created_at_not_changed}
|
10
|
+
before_save lambda {|model| model.updated_at = Time.now; model.updated_at_not_changed}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/simple_property'
|
2
|
+
require File.dirname(__FILE__) + '/belongs_to_property'
|
3
|
+
|
4
|
+
module CouchTomato
|
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, meta={}) #:nodoc:
|
31
|
+
return if json.nil?
|
32
|
+
instance = super
|
33
|
+
# instance.send(:assign_attribute_copies_for_dirty_tracking)
|
34
|
+
instance.metadata = meta
|
35
|
+
instance
|
36
|
+
end
|
37
|
+
|
38
|
+
# 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.
|
39
|
+
#
|
40
|
+
# example:
|
41
|
+
# class Book
|
42
|
+
# property :title
|
43
|
+
# property :year
|
44
|
+
# property :publisher, :class => Publisher
|
45
|
+
# end
|
46
|
+
def property(name, options = {})
|
47
|
+
clazz = options.delete(:class)
|
48
|
+
properties << (clazz || SimpleProperty).new(self, name, options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def belongs_to(name) #:nodoc:
|
52
|
+
property name, :class => BelongsToProperty
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module CouchTomato
|
2
|
+
module Persistence
|
3
|
+
class SimpleProperty #:nodoc:
|
4
|
+
attr_accessor :name, :type
|
5
|
+
JSON_TYPES = [String, Integer, Hash, Array, Fixnum, Float]
|
6
|
+
|
7
|
+
def initialize(owner_clazz, name, options = {})
|
8
|
+
if JSON_TYPES.include?(options[:type])
|
9
|
+
raise "#{options[:type]} is a native JSON type, only custom types should be specified"
|
10
|
+
end
|
11
|
+
|
12
|
+
if options[:type].kind_of?(Array) && options[:type].empty?
|
13
|
+
raise "property defined with `:type => []` but expected `:type => [SomePersistableType]`"
|
14
|
+
end
|
15
|
+
|
16
|
+
self.name = name
|
17
|
+
self.type = options[:type]
|
18
|
+
owner_clazz.class_eval do
|
19
|
+
attr_reader name, "#{name}_was"
|
20
|
+
|
21
|
+
def initialize(attributes = {})
|
22
|
+
super attributes
|
23
|
+
# assign_attribute_copies_for_dirty_tracking
|
24
|
+
end
|
25
|
+
|
26
|
+
# def assign_attribute_copies_for_dirty_tracking
|
27
|
+
# attributes.each do |name, value|
|
28
|
+
# self.instance_variable_set("@#{name}_was", clone_attribute(value))
|
29
|
+
# end if attributes
|
30
|
+
# end
|
31
|
+
# private :assign_attribute_copies_for_dirty_tracking
|
32
|
+
|
33
|
+
def clone_attribute(value)
|
34
|
+
if [Bignum, Fixnum, Symbol, TrueClass, FalseClass, NilClass, Float].include?(value.class)
|
35
|
+
value
|
36
|
+
else
|
37
|
+
value.clone
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
define_method "#{name}=" do |value|
|
42
|
+
self.instance_variable_set("@#{name}", value)
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method "#{name}?" do
|
46
|
+
!self.send(name).nil? && !self.send(name).try(:blank?)
|
47
|
+
end
|
48
|
+
|
49
|
+
define_method "#{name}_changed?" do
|
50
|
+
!self.instance_variable_get("@#{name}_not_changed") && self.send(name) != self.send("#{name}_was")
|
51
|
+
end
|
52
|
+
|
53
|
+
define_method "#{name}_not_changed" do
|
54
|
+
self.instance_variable_set("@#{name}_not_changed", true)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def build(object, json)
|
60
|
+
value = json[name.to_s]
|
61
|
+
value = json[name.to_sym] if value.nil?
|
62
|
+
|
63
|
+
if type.kind_of? Array
|
64
|
+
typecasted_value = []
|
65
|
+
value.each do |val|
|
66
|
+
el = type[0].json_create val
|
67
|
+
typecasted_value << el
|
68
|
+
end
|
69
|
+
else
|
70
|
+
typecasted_value = if type
|
71
|
+
type.json_create value
|
72
|
+
else
|
73
|
+
value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
object.send "#{name}=", typecasted_value
|
78
|
+
end
|
79
|
+
|
80
|
+
def dirty?(object)
|
81
|
+
object.send("#{name}_changed?")
|
82
|
+
end
|
83
|
+
|
84
|
+
# def save(object)
|
85
|
+
#
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# def destroy(object)
|
89
|
+
#
|
90
|
+
# end
|
91
|
+
|
92
|
+
def serialize(json, object)
|
93
|
+
json[name] = object.send name
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'validatable'
|
2
|
+
|
3
|
+
module CouchTomato
|
4
|
+
module Persistence
|
5
|
+
module Validation
|
6
|
+
def self.included(base)
|
7
|
+
base.send :include, Validatable
|
8
|
+
base.class_eval do
|
9
|
+
# Override the validate method to first run before_validation callback
|
10
|
+
def valid?
|
11
|
+
self.run_callbacks :before_validation
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|