langalex-couch_potato 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/README.md +260 -0
  2. data/VERSION.yml +2 -2
  3. data/init.rb +3 -0
  4. data/lib/core_ext/date.rb +10 -0
  5. data/lib/core_ext/string.rb +15 -0
  6. data/lib/core_ext/time.rb +6 -9
  7. data/lib/couch_potato/database.rb +90 -0
  8. data/lib/couch_potato/persistence/belongs_to_property.rb +1 -1
  9. data/lib/couch_potato/persistence/callbacks.rb +14 -11
  10. data/lib/couch_potato/persistence/dirty_attributes.rb +13 -6
  11. data/lib/couch_potato/persistence/json.rb +9 -14
  12. data/lib/couch_potato/persistence/magic_timestamps.rb +13 -0
  13. data/lib/couch_potato/persistence/properties.rb +10 -8
  14. data/lib/couch_potato/persistence/simple_property.rb +14 -11
  15. data/lib/couch_potato/persistence.rb +11 -165
  16. data/lib/couch_potato/view/base_view_spec.rb +20 -0
  17. data/lib/couch_potato/view/custom_view_spec.rb +26 -0
  18. data/lib/couch_potato/view/custom_views.rb +30 -0
  19. data/lib/couch_potato/view/model_view_spec.rb +39 -0
  20. data/lib/couch_potato/view/properties_view_spec.rb +35 -0
  21. data/lib/couch_potato/view/raw_view_spec.rb +21 -0
  22. data/lib/couch_potato/view/view_query.rb +45 -0
  23. data/lib/couch_potato.rb +23 -6
  24. data/rails/init.rb +7 -0
  25. data/spec/callbacks_spec.rb +40 -43
  26. data/spec/create_spec.rb +9 -60
  27. data/spec/custom_view_spec.rb +93 -19
  28. data/spec/destroy_spec.rb +5 -4
  29. data/spec/property_spec.rb +22 -8
  30. data/spec/spec_helper.rb +12 -14
  31. data/spec/unit/attributes_spec.rb +26 -0
  32. data/spec/unit/create_spec.rb +58 -0
  33. data/spec/{dirty_attributes_spec.rb → unit/dirty_attributes_spec.rb} +31 -13
  34. data/spec/unit/string_spec.rb +13 -0
  35. data/spec/unit/view_query_spec.rb +2 -3
  36. data/spec/update_spec.rb +8 -7
  37. metadata +54 -32
  38. data/README.textile +0 -340
  39. data/lib/couch_potato/active_record/compatibility.rb +0 -9
  40. data/lib/couch_potato/ordering.rb +0 -84
  41. data/lib/couch_potato/persistence/bulk_save_queue.rb +0 -47
  42. data/lib/couch_potato/persistence/collection.rb +0 -51
  43. data/lib/couch_potato/persistence/custom_view.rb +0 -41
  44. data/lib/couch_potato/persistence/external_collection.rb +0 -83
  45. data/lib/couch_potato/persistence/external_has_many_property.rb +0 -72
  46. data/lib/couch_potato/persistence/find.rb +0 -21
  47. data/lib/couch_potato/persistence/finder.rb +0 -65
  48. data/lib/couch_potato/persistence/inline_has_many_property.rb +0 -43
  49. data/lib/couch_potato/persistence/view_query.rb +0 -81
  50. data/lib/couch_potato/versioning.rb +0 -46
  51. data/spec/attributes_spec.rb +0 -42
  52. data/spec/belongs_to_spec.rb +0 -55
  53. data/spec/find_spec.rb +0 -96
  54. data/spec/finder_spec.rb +0 -125
  55. data/spec/has_many_spec.rb +0 -241
  56. data/spec/inline_collection_spec.rb +0 -15
  57. data/spec/ordering_spec.rb +0 -95
  58. data/spec/reload_spec.rb +0 -50
  59. data/spec/unit/external_collection_spec.rb +0 -84
  60. data/spec/unit/finder_spec.rb +0 -10
  61. data/spec/versioning_spec.rb +0 -150
data/README.md ADDED
@@ -0,0 +1,260 @@
1
+ ## Couch Potato
2
+
3
+ ... is a persistence layer written in ruby for CouchDB.
4
+
5
+ ### Mission
6
+
7
+ The goal of Couch Potato is to create a minimal framework in order to store and retrieve Ruby objects to/from CouchDB and create and query views.
8
+
9
+ It follows the document/view/querying semantics established by CouchDB and won't try to mimic ActiveRecord behavior in any way as that IS BAD.
10
+
11
+ Code that uses Couch Potato should be easy to test.
12
+
13
+ Lastly Couch Potato aims to provide a seamless integration with Ruby on Rails, e.g. routing, form helpers etc.
14
+
15
+ ### Core Features
16
+
17
+ * persisting objects by including the CouchPotato::Persistence module
18
+ * declarative views with either custom or generated map/reduce functions
19
+ * extensive spec suite
20
+
21
+ ### Installation
22
+
23
+ Couch Potato requires Ruby 1.9.
24
+
25
+ Couch Potato is hosted as a gem on github which you can install like this:
26
+
27
+ sudo gem source --add http://gems.github.com # if you haven't already
28
+ sudo gem install langalex-couch_potato
29
+
30
+ #### Using with your ruby application:
31
+
32
+ require 'rubygems'
33
+ gem 'langalex-couch_potato'
34
+ require 'couch_potato'
35
+ CouchPotato::Config.database_name = 'name of the db'
36
+
37
+ Alternatively you can download or clone the source repository and then require lib/couch_potato.rb.
38
+
39
+ #### Using with Rails
40
+
41
+ Add to your config/environment.rb:
42
+
43
+ config.gem 'langalex-couch_potato', :lib => 'couch_potato', :source => 'http://gems.github.com'
44
+
45
+ Then create a config/couchdb.yml:
46
+
47
+ development: development_db_name
48
+ test: test_db_name
49
+ production: http://db.server/production_db_name
50
+
51
+ Alternatively you can also install Couch Potato directly as a plugin.
52
+
53
+ ### Introduction
54
+
55
+ This is a basic tutorial on how to use Couch Potato. If you want to know all the details feel free to read the specs.
56
+
57
+ #### Save, load objects
58
+
59
+ First you need a class.
60
+
61
+ class User
62
+ end
63
+
64
+ To make instances of this class persistent include the persistence module:
65
+
66
+ class User
67
+ include CouchPotato::Persistence
68
+ end
69
+
70
+ If you want to store any properties you have to declare them:
71
+
72
+ class User
73
+ include CouchPotato::Persistence
74
+
75
+ property :name
76
+ end
77
+
78
+ Properties can be of any type:
79
+
80
+ class User
81
+ include CouchPotato::Persistence
82
+
83
+ property :address, :type => Address
84
+ end
85
+
86
+ Now you can save your objects. All database operations are encapsulated in the CouchPotato::Database class. This separates your domain logic from the database access logic which makes it easier to write tests and also keeps you models smaller and cleaner.
87
+
88
+ user = User.new :name => 'joe'
89
+ CouchPotato.database.save_document user # or save_document!
90
+
91
+ You can of course also retrieve your instance:
92
+
93
+ CouchPotato.database.load_document "id_of_the_user_document" # => <#User 0x3075>
94
+
95
+
96
+ #### Properties
97
+
98
+ You can access the properties you declared above through normal attribute accessors.
99
+
100
+ user.name # => 'joe'
101
+ user.name = {:first => ['joe', 'joey'], :last => 'doe', :middle => 'J'} # you can set any ruby object that responds_to :to_json (includes all core objects)
102
+ user._id # => "02097f33a0046123f1ebc0ebb6937269"
103
+ user._rev # => "2769180384"
104
+ user.created_at # => Fri Oct 24 19:05:54 +0200 2008
105
+ user.updated_at # => Fri Oct 24 19:05:54 +0200 2008
106
+ user.new? # => false
107
+
108
+ If you want to have properties that don't map to any JSON type, i.e. other than String, Number, Boolean, Hash or Array you have to define the type like this:
109
+
110
+ class User
111
+ property :date_of_birth, :type => Date
112
+ end
113
+
114
+ The date_of_birth property is now automatically serialized to JSON and back when storing/retrieving objects.
115
+
116
+ #### Dirty tracking
117
+
118
+ CouchPotato tracks the dirty state of attributes in the same way ActiveRecord does:
119
+
120
+ user = User.create :name => 'joe'
121
+ user.name # => 'joe'
122
+ user.name_changed? # => false
123
+ user.name_was # => nil
124
+
125
+ You can also force a dirty state:
126
+
127
+ user.name = 'jane'
128
+ user.name_changed? # => true
129
+ user.name_not_changed
130
+ user.name_changed? # => false
131
+ CouchPotato.database.save_document user # does nothing as no attributes are dirty
132
+
133
+
134
+ #### Object validations
135
+
136
+ Couch Potato uses the validatable library for vaidation (http://validatable.rubyforge.org/)\
137
+
138
+ class User
139
+ property :name
140
+ validates_presence_of :name
141
+ end
142
+
143
+ user = User.new
144
+ user.valid? # => false
145
+ user.errors.on(:name) # => [:name, 'can't be blank']
146
+
147
+ #### Finding stuff
148
+
149
+ In order to find data in your CouchDB you have to create a view first. Couch Potato offers you to create and manage those views for you. All you have to do is declare them in your classes:
150
+
151
+ class User
152
+ include CouchPotato::Persistence
153
+ property :name
154
+
155
+ view :all, :key => :created_at
156
+ end
157
+
158
+ This will create a view called "all" in the "user" design document with a map function that emits "created_at" for every user document.
159
+
160
+ CouchPotato.database.view User.all
161
+
162
+ This will load all user documents in your database sorted by created_at.
163
+
164
+ CouchPotato.database.view User.all(:key => (Time.now- 10)..(Time.now), :descending => true)
165
+
166
+ Any options you pass in will be passed onto CouchDB.
167
+
168
+ Composite keys are also possible:
169
+
170
+ class User
171
+ property :name
172
+
173
+ view :all, :key => [:created_at, :name]
174
+ end
175
+
176
+ The creation of views is based on view specification classes (see the CouchPotato::View) module. The above code used the ModelViewSpec class which is used to the simple find model by property searches. For more sophisticated searches you can use other view specifications (either use the built-in or provide your own) by passing a type parameter:
177
+
178
+ If you have larger structures and you only want to load some attributes you can customize the view you can use the PropertiesViewSpec (the full class name is automatically derived):
179
+
180
+ class User
181
+ property :name
182
+ property :bio
183
+
184
+ view :all, :key => :created_at, :properties => [:name], :type => :properties
185
+ end
186
+
187
+ CouchPotato.database.view(User.everyone).first.name # => "joe"
188
+ CouchPotato.database.view(User.everyone).first.bio # => nil
189
+
190
+ You can also pass in custom map/reduce functions with the custom view spec:
191
+
192
+ class User
193
+ view :all, :map => "function(doc) { emit(doc.created_at, null)}", :include_docs => true, :type => :custom
194
+ end
195
+
196
+ If you don't want the results to be converted into models the raw view is your friend:
197
+
198
+ class User
199
+ view :all, :map => "function(doc) { emit(doc.created_at, doc.name)}", :type => :raw
200
+ end
201
+
202
+ When querying this view you will get the raw data returned by CouchDB which looks something like this: {'total_entries': 2, 'rows': [{'value': 'alex', 'key': '2009-01-03 00:02:34 +000', 'id': '75976rgi7546gi02a'}]}
203
+
204
+ To process this raw data you can also pass in a results filter:
205
+
206
+ class User
207
+ view :all, :map => "function(doc) { emit(doc.created_at, doc.name)}", :type => :raw, :results_filter => lambda {|results| results['rows'].map{|row| row['value']}}
208
+ end
209
+
210
+ In this case querying the view would only return the emitted value for each row.
211
+
212
+ #### Associations
213
+
214
+ Not supported. Not sure if they ever will be. You can implement those yourself using views and custom methods on your models.
215
+
216
+ #### Callbacks
217
+
218
+ Couch Potato supports the usual lifecycle callbacks known from ActiveRecord:
219
+
220
+ class User
221
+ include CouchPotato::Persistence
222
+
223
+ before_create :do_something_before_create
224
+ after_update :do_something_else
225
+ end
226
+
227
+ This will call the method do_something_before_create before creating an object and do_something_else after updating one. Supported callbacks are: :before_validation_on_create, :before_validation_on_update, :before_validation_on_save, :before_create, :after_create, :before_update, :after_update, :before_save, :after_save, :before_destroy, :after_destroy. You can also pass a Proc instead of a method name.
228
+
229
+ #### Testing
230
+
231
+ To make testing easier and faster database logic has been put into its own class, which you can replace and stub out in whatever way you want:
232
+
233
+ class User
234
+ include CouchPotato::Persistence
235
+ end
236
+
237
+ # RSpec
238
+ describe 'save a user' do
239
+ it 'should save' do
240
+ couchrest_db = stub 'couchrest_db',
241
+ database = CouchPotato::Database.new couchrest_db
242
+ user = User.new
243
+ couchrest_db.should_receive(:save_doc).with(...)
244
+ database.save_document user
245
+ end
246
+ end
247
+
248
+ By creating you own instances of CouchPotato::Database and passing them a fake CouchRest database instance you can completely disconnect your unit tests/spec from the database.
249
+
250
+ ### Helping out
251
+
252
+ Please fix bugs, add more specs, implement new features by forking the github repo at http://github.com/langalex/couch_potato.
253
+
254
+ You can run all the specs by calling 'rake spec_unit' and 'rake spec_functional' in the root folder of Couch Potato. The specs require a running CouchDB instance at http://localhost:5984
255
+
256
+ I will only accept patches that are covered by specs - sorry.
257
+
258
+ ### Contact
259
+
260
+ If you have any questions/suggestions etc. please contact me at alex at upstream-berlin.com or @langalex on twitter.
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 1
3
2
  :major: 0
4
- :minor: 1
3
+ :minor: 2
4
+ :patch: 0
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ # this is for rails only
2
+
3
+ require File.dirname(__FILE__) + '/rails/init'
@@ -0,0 +1,10 @@
1
+ class Date
2
+ def to_json(*a)
3
+ %("#{strftime("%Y/%m/%d")}")
4
+ end
5
+
6
+ def self.json_create string
7
+ return nil if string.nil?
8
+ Date.parse(string)
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveSupportMethods
2
+ def camelize
3
+ sub(/^([a-z])/) {$1.upcase}.gsub(/_([a-z])/) do
4
+ $1.upcase
5
+ end
6
+ end
7
+
8
+ def underscore
9
+ gsub(/([A-Z])/) do
10
+ '_' + $1.downcase
11
+ end.sub(/^_/, '')
12
+ end
13
+ end
14
+
15
+ String.send :include, ActiveSupportMethods #unless String.new.respond_to?(:camelize)
data/lib/core_ext/time.rb CHANGED
@@ -1,14 +1,11 @@
1
1
  class Time
2
2
  def to_json(*a)
3
- {
4
- 'json_class' => self.class.name,
5
- 'data' => self.strftime("%Y/%m/%d %H:%M:%S %z")
6
- }.to_json(*a)
3
+ %("#{strftime("%Y/%m/%d %H:%M:%S +0000")}")
7
4
  end
8
- end
9
-
10
- class Time
11
- def self.json_create(o)
12
- parse(*o['data'])
5
+
6
+ def self.json_create string
7
+ return nil if string.nil?
8
+ d = DateTime.parse(string).new_offset
9
+ self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec)
13
10
  end
14
11
  end
@@ -0,0 +1,90 @@
1
+ module CouchPotato
2
+ class Database
3
+
4
+ class ValidationsFailedError < ::Exception; end
5
+
6
+ def initialize(couchrest_database)
7
+ @database = couchrest_database
8
+ end
9
+
10
+ def view(spec)
11
+ results = CouchPotato::View::ViewQuery.new(database,
12
+ spec.design_document, spec.view_name, spec.map_function,
13
+ spec.reduce_function).query_view!(spec.view_parameters)
14
+ spec.process_results results
15
+ end
16
+
17
+ def save_document(document)
18
+ return unless document.dirty?
19
+ if document.new?
20
+ create_document document
21
+ else
22
+ update_document document
23
+ end
24
+ end
25
+ alias_method :save, :save_document
26
+
27
+ def save_document!(document)
28
+ save_document(document) || raise(ValidationsFailedError.new(document.errors.full_messages))
29
+ end
30
+ alias_method :save!, :save_document!
31
+
32
+ def destroy_document(document)
33
+ document.run_callbacks(:before_destroy)
34
+ document._deleted = true
35
+ database.delete_doc document.to_hash
36
+ document.run_callbacks(:after_destroy)
37
+ document._id = nil
38
+ document._rev = nil
39
+ end
40
+ alias_method :destroy, :destroy_document
41
+
42
+ def load_document(id)
43
+ begin
44
+ json = database.get(id)
45
+ Class.const_get(json['ruby_class']).json_create json
46
+ rescue(RestClient::ResourceNotFound)
47
+ nil
48
+ end
49
+ end
50
+ alias_method :load, :load_document
51
+
52
+ def inspect
53
+ "#<CouchPotato::Database>"
54
+ end
55
+
56
+ private
57
+
58
+ def create_document(document)
59
+ document.run_callbacks :before_validation_on_save
60
+ document.run_callbacks :before_validation_on_create
61
+ return unless document.valid?
62
+ document.run_callbacks :before_save
63
+ document.run_callbacks :before_create
64
+ res = database.save_doc document.to_hash
65
+ document._rev = res['rev']
66
+ document._id = res['id']
67
+ document.run_callbacks :after_save
68
+ document.run_callbacks :after_create
69
+ true
70
+ end
71
+
72
+ def update_document(document)
73
+ document.run_callbacks(:before_validation_on_save)
74
+ document.run_callbacks(:before_validation_on_update)
75
+ return unless document.valid?
76
+ document.run_callbacks :before_save
77
+ document.run_callbacks :before_update
78
+ res = database.save_doc document.to_hash
79
+ document._rev = res['rev']
80
+ document.run_callbacks :after_save
81
+ document.run_callbacks :after_update
82
+ true
83
+ end
84
+
85
+ def database
86
+ @database
87
+ end
88
+
89
+ end
90
+ end
@@ -50,7 +50,7 @@ module CouchPotato
50
50
  end
51
51
 
52
52
  def item_class_name
53
- @name.to_s.singularize.camelcase
53
+ @name.to_s.camelize
54
54
  end
55
55
 
56
56
  end
@@ -7,8 +7,8 @@ module CouchPotato
7
7
  base.class_eval do
8
8
  attr_accessor :skip_callbacks
9
9
  def self.callbacks
10
- @@callbacks ||= {}
11
- @@callbacks[self.name] ||= {:before_validation_on_create => [],
10
+ @callbacks ||= {}
11
+ @callbacks[self.name] ||= {:before_validation_on_create => [],
12
12
  :before_validation_on_update => [], :before_validation_on_save => [], :before_create => [],
13
13
  :after_create => [], :before_update => [], :after_update => [],
14
14
  :before_save => [], :after_save => [],
@@ -17,19 +17,22 @@ module CouchPotato
17
17
  end
18
18
  end
19
19
 
20
- def save_without_callbacks
21
- self.skip_callbacks = true
22
- result = save
23
- self.skip_callbacks = false
24
- result
20
+ def run_callbacks(name)
21
+ return if skip_callbacks
22
+ self.class.callbacks[name].uniq.each do |callback|
23
+ run_callback callback
24
+ end
25
25
  end
26
26
 
27
27
  private
28
28
 
29
- def run_callbacks(name)
30
- return if skip_callbacks
31
- self.class.callbacks[name].each do |callback|
32
- self.send callback
29
+ def run_callback(name)
30
+ if name.is_a?(Symbol)
31
+ self.send name
32
+ elsif name.is_a?(Proc)
33
+ name.call self
34
+ else
35
+ raise "Don't know how to handle callback of type #{name.class.name}"
33
36
  end
34
37
  end
35
38
 
@@ -1,19 +1,26 @@
1
1
  module CouchPotato
2
2
  module Persistence
3
3
  module DirtyAttributes
4
- def save
5
- if dirty?
6
- super
7
- else
8
- valid?
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ after_save :reset_dirty_attributes
9
8
  end
10
9
  end
11
10
 
12
11
  def dirty?
13
- new_document? || self.class.properties.inject(false) do |res, property|
12
+ new? || self.class.properties.inject(false) do |res, property|
14
13
  res || property.dirty?(self)
15
14
  end
16
15
  end
16
+
17
+ private
18
+
19
+ def reset_dirty_attributes
20
+ self.class.properties.each do |property|
21
+ instance_variable_set("@#{property.name}_was", send(property.name))
22
+ end
23
+ end
17
24
  end
18
25
  end
19
26
  end
@@ -6,37 +6,32 @@ module CouchPotato
6
6
  end
7
7
 
8
8
  def to_json(*args)
9
+ to_hash.to_json(*args)
10
+ end
11
+
12
+ def to_hash
9
13
  (self.class.properties).inject({}) do |props, property|
10
14
  property.serialize(props, self)
11
15
  props
12
- end.merge('ruby_class' => self.class.name).merge(id_and_rev_json).merge(timestamps_json).to_json(*args)
16
+ end.merge('ruby_class' => self.class.name).merge(id_and_rev_json)
13
17
  end
14
18
 
15
19
  private
16
20
 
17
21
  def id_and_rev_json
18
- [:_id, :_rev, :_deleted].inject({}) do |hash, key|
22
+ ['_id', '_rev', '_deleted'].inject({}) do |hash, key|
19
23
  hash[key] = self.send(key) unless self.send(key).nil?
20
24
  hash
21
25
  end
22
26
  end
23
27
 
24
- def timestamps_json
25
- [:created_at, :updated_at].inject({}) do |hash, key|
26
- hash[key] = self.send(key).to_s unless self.send(key).nil?
27
- hash
28
- end
29
- end
30
-
31
28
  module ClassMethods
32
29
  def json_create(json)
33
30
  instance = self.new
34
- instance.created_at = Time.parse(json['created_at'])
35
- instance.updated_at = Time.parse(json['updated_at'])
36
- instance._id = json['_id']
37
- instance._rev = json['_rev']
31
+ instance._id = json[:_id] || json['_id']
32
+ instance._rev = json[:_rev] || json['_rev']
38
33
  properties.each do |property|
39
- property.build(instance, json) unless property.is_a?(ExternalHasManyProperty)
34
+ property.build(instance, json)
40
35
  end
41
36
  instance
42
37
  end
@@ -0,0 +1,13 @@
1
+ module CouchPotato
2
+ module MagicTimestamps
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
@@ -1,7 +1,5 @@
1
1
  require File.dirname(__FILE__) + '/simple_property'
2
2
  require File.dirname(__FILE__) + '/belongs_to_property'
3
- require File.dirname(__FILE__) + '/inline_has_many_property'
4
- require File.dirname(__FILE__) + '/external_has_many_property'
5
3
 
6
4
  module CouchPotato
7
5
  module Persistence
@@ -10,8 +8,8 @@ module CouchPotato
10
8
  base.extend ClassMethods
11
9
  base.class_eval do
12
10
  def self.properties
13
- @@properties ||= {}
14
- @@properties[self.name] ||= []
11
+ @properties ||= {}
12
+ @properties[self.name] ||= []
15
13
  end
16
14
  end
17
15
  end
@@ -21,6 +19,14 @@ module CouchPotato
21
19
  properties.map(&:name)
22
20
  end
23
21
 
22
+ def json_create(json)
23
+ instance = super
24
+ instance.attributes.each do |name, value|
25
+ instance.instance_variable_set("@#{name}_was", value)
26
+ end
27
+ instance
28
+ end
29
+
24
30
  def property(name, options = {})
25
31
  clazz = options.delete(:class)
26
32
  properties << (clazz || SimpleProperty).new(self, name, options)
@@ -30,10 +36,6 @@ module CouchPotato
30
36
  property name, :class => BelongsToProperty
31
37
  end
32
38
 
33
- def has_many(name, options = {})
34
- stored = options.delete(:stored)
35
- property name, options.merge(:class => (stored == :inline ? InlineHasManyProperty : ExternalHasManyProperty))
36
- end
37
39
  end
38
40
  end
39
41
  end
@@ -1,10 +1,11 @@
1
1
  module CouchPotato
2
2
  module Persistence
3
3
  class SimpleProperty
4
- attr_accessor :name
4
+ attr_accessor :name, :type
5
5
 
6
6
  def initialize(owner_clazz, name, options = {})
7
7
  self.name = name
8
+ self.type = options[:type]
8
9
  owner_clazz.class_eval do
9
10
  attr_reader name, "#{name}_was"
10
11
 
@@ -15,14 +16,6 @@ module CouchPotato
15
16
  end if attributes
16
17
  end
17
18
 
18
- def self.json_create(json)
19
- instance = super
20
- instance.attributes.each do |name, value|
21
- instance.instance_variable_set("@#{name}_was", value)
22
- end
23
- instance
24
- end
25
-
26
19
  define_method "#{name}=" do |value|
27
20
  self.instance_variable_set("@#{name}", value)
28
21
  end
@@ -32,13 +25,23 @@ module CouchPotato
32
25
  end
33
26
 
34
27
  define_method "#{name}_changed?" do
35
- self.send(name) != self.send("#{name}_was")
28
+ !self.instance_variable_get("@#{name}_not_changed") && self.send(name) != self.send("#{name}_was")
29
+ end
30
+
31
+ define_method "#{name}_not_changed" do
32
+ self.instance_variable_set("@#{name}_not_changed", true)
36
33
  end
37
34
  end
38
35
  end
39
36
 
40
37
  def build(object, json)
41
- object.send "#{name}=", json.stringify_keys[name.to_s]
38
+ value = json[name.to_s] || json[name.to_sym]
39
+ typecasted_value = if type
40
+ type.json_create value
41
+ else
42
+ value
43
+ end
44
+ object.send "#{name}=", typecasted_value
42
45
  end
43
46
 
44
47
  def dirty?(object)