friendly 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,11 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ### (0.4.4)
5
+
6
+ * (jamesgolick) Make it possible to query with order, but no conditions.
7
+ * (jamesgolick) Add change tracking. This is mostly to facilitate arbitrary caches.
8
+
4
9
  ### 0.4.2
5
10
 
6
11
  * (nullstyle) convert UUID to SQL::Blob so that Sequel can properly escape it in databases that don't treat binary strings like regular strings.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.3
1
+ 0.4.4
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{friendly}
8
- s.version = "0.4.3"
8
+ s.version = "0.4.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["James Golick"]
12
- s.date = %q{2009-12-24}
12
+ s.date = %q{2010-01-16}
13
13
  s.description = %q{}
14
14
  s.email = %q{jamesgolick@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -37,13 +37,17 @@ Gem::Specification.new do |s|
37
37
  "lib/friendly/boolean.rb",
38
38
  "lib/friendly/cache.rb",
39
39
  "lib/friendly/cache/by_id.rb",
40
- "lib/friendly/config.rb",
41
40
  "lib/friendly/data_store.rb",
42
41
  "lib/friendly/document.rb",
42
+ "lib/friendly/document/associations.rb",
43
+ "lib/friendly/document/attributes.rb",
44
+ "lib/friendly/document/convenience.rb",
45
+ "lib/friendly/document/mixin.rb",
46
+ "lib/friendly/document/scoping.rb",
47
+ "lib/friendly/document/storage.rb",
43
48
  "lib/friendly/document_table.rb",
44
49
  "lib/friendly/index.rb",
45
50
  "lib/friendly/memcached.rb",
46
- "lib/friendly/named_scope.rb",
47
51
  "lib/friendly/newrelic.rb",
48
52
  "lib/friendly/query.rb",
49
53
  "lib/friendly/scope.rb",
@@ -71,6 +75,7 @@ Gem::Specification.new do |s|
71
75
  "spec/integration/convenience_api_spec.rb",
72
76
  "spec/integration/count_spec.rb",
73
77
  "spec/integration/default_value_spec.rb",
78
+ "spec/integration/dirty_tracking_spec.rb",
74
79
  "spec/integration/find_via_cache_spec.rb",
75
80
  "spec/integration/finder_spec.rb",
76
81
  "spec/integration/has_many_spec.rb",
@@ -87,14 +92,13 @@ Gem::Specification.new do |s|
87
92
  "spec/unit/attribute_spec.rb",
88
93
  "spec/unit/cache_by_id_spec.rb",
89
94
  "spec/unit/cache_spec.rb",
90
- "spec/unit/config_spec.rb",
91
95
  "spec/unit/data_store_spec.rb",
96
+ "spec/unit/document/attributes_spec.rb",
92
97
  "spec/unit/document_spec.rb",
93
98
  "spec/unit/document_table_spec.rb",
94
99
  "spec/unit/friendly_spec.rb",
95
100
  "spec/unit/index_spec.rb",
96
101
  "spec/unit/memcached_spec.rb",
97
- "spec/unit/named_scope_spec.rb",
98
102
  "spec/unit/query_spec.rb",
99
103
  "spec/unit/scope_proxy_spec.rb",
100
104
  "spec/unit/scope_spec.rb",
@@ -164,6 +168,7 @@ Gem::Specification.new do |s|
164
168
  "spec/integration/convenience_api_spec.rb",
165
169
  "spec/integration/count_spec.rb",
166
170
  "spec/integration/default_value_spec.rb",
171
+ "spec/integration/dirty_tracking_spec.rb",
167
172
  "spec/integration/find_via_cache_spec.rb",
168
173
  "spec/integration/finder_spec.rb",
169
174
  "spec/integration/has_many_spec.rb",
@@ -179,14 +184,13 @@ Gem::Specification.new do |s|
179
184
  "spec/unit/attribute_spec.rb",
180
185
  "spec/unit/cache_by_id_spec.rb",
181
186
  "spec/unit/cache_spec.rb",
182
- "spec/unit/config_spec.rb",
183
187
  "spec/unit/data_store_spec.rb",
188
+ "spec/unit/document/attributes_spec.rb",
184
189
  "spec/unit/document_spec.rb",
185
190
  "spec/unit/document_table_spec.rb",
186
191
  "spec/unit/friendly_spec.rb",
187
192
  "spec/unit/index_spec.rb",
188
193
  "spec/unit/memcached_spec.rb",
189
- "spec/unit/named_scope_spec.rb",
190
194
  "spec/unit/query_spec.rb",
191
195
  "spec/unit/scope_proxy_spec.rb",
192
196
  "spec/unit/scope_spec.rb",
@@ -59,18 +59,25 @@ module Friendly
59
59
  nil
60
60
  end
61
61
  end
62
+
63
+ def assign_default_value(document)
64
+ document.send(:"#{name}=", default)
65
+ end
62
66
 
63
67
  protected
64
68
  def build_accessors
65
69
  n = name
66
70
  klass.class_eval do
71
+ attr_reader n, :"#{n}_was"
72
+
67
73
  eval <<-__END__
68
74
  def #{n}=(value)
75
+ will_change(:#{n})
69
76
  @#{n} = self.class.attributes[:#{n}].typecast(value)
70
77
  end
71
78
 
72
- def #{n}
73
- @#{n} ||= self.class.attributes[:#{n}].default
79
+ def #{n}_changed?
80
+ attribute_changed?(:#{n})
74
81
  end
75
82
  __END__
76
83
  end
@@ -12,7 +12,8 @@ module Friendly
12
12
  end
13
13
 
14
14
  def all(persistable, query)
15
- filtered = dataset(persistable).where(query.conditions)
15
+ filtered = dataset(persistable)
16
+ filtered = filtered.where(query.conditions) unless query.conditions.empty?
16
17
  if query.limit || query.offset
17
18
  filtered = filtered.limit(query.limit, query.offset)
18
19
  end
@@ -1,5 +1,9 @@
1
1
  require 'active_support/inflector'
2
- require 'friendly/associations'
2
+ require 'friendly/document/associations'
3
+ require 'friendly/document/attributes'
4
+ require 'friendly/document/convenience'
5
+ require 'friendly/document/scoping'
6
+ require 'friendly/document/storage'
3
7
 
4
8
  module Friendly
5
9
  module Document
@@ -26,198 +30,18 @@ module Friendly
26
30
  end
27
31
 
28
32
  module ClassMethods
29
- attr_writer :storage_proxy, :query_klass,
30
- :table_name, :collection_klass,
31
- :scope_proxy, :association_set
32
-
33
- def create_tables!
34
- storage_proxy.create_tables!
35
- end
36
-
37
- def attribute(name, type = nil, options = {})
38
- attributes[name] = Attribute.new(self, name, type, options)
39
- end
40
-
41
- def storage_proxy
42
- @storage_proxy ||= StorageProxy.new(self)
43
- end
44
-
45
- def query_klass
46
- @query_klass ||= Query
47
- end
48
-
49
- def collection_klass
50
- @collection_klass ||= WillPaginate::Collection
51
- end
52
-
53
- def indexes(*args)
54
- storage_proxy.add(args)
55
- end
56
-
57
- def caches_by(*fields)
58
- options = fields.last.is_a?(Hash) ? fields.pop : {}
59
- storage_proxy.cache(fields, options)
60
- end
61
-
62
- def attributes
63
- @attributes ||= {}
64
- end
65
-
66
- def first(query)
67
- storage_proxy.first(query(query))
68
- end
69
-
70
- def all(query)
71
- storage_proxy.all(query(query))
72
- end
73
-
74
- def find(id)
75
- doc = first(:id => id)
76
- raise RecordNotFound, "Couldn't find #{name}/#{id}" if doc.nil?
77
- doc
78
- end
79
-
80
- def count(conditions)
81
- storage_proxy.count(query(conditions))
82
- end
83
-
84
- def paginate(conditions)
85
- query = query(conditions)
86
- count = count(query)
87
- collection = collection_klass.new(query.page, query.per_page, count)
88
- collection.replace(all(query))
89
- end
90
-
91
- def create(attributes = {})
92
- doc = new(attributes)
93
- doc.save
94
- doc
95
- end
33
+ attr_writer :table_name
96
34
 
97
35
  def table_name
98
36
  @table_name ||= name.pluralize.underscore
99
37
  end
100
-
101
- def scope_proxy
102
- @scope_proxy ||= ScopeProxy.new(self)
103
- end
104
-
105
- # Add a named scope to this Document.
106
- #
107
- # e.g.
108
- #
109
- # class Post
110
- # indexes :created_at
111
- # named_scope :recent, :order! => :created_at.desc
112
- # end
113
- #
114
- # Then, you can access the recent posts with:
115
- #
116
- # Post.recent.all
117
- # ...or...
118
- # Post.recent.first
119
- #
120
- # Both #all and #first also take additional parameters:
121
- #
122
- # Post.recent.all(:author_id => @author.id)
123
- #
124
- # Scopes are also chainable. See the README or Friendly::Scope docs for details.
125
- #
126
- # @param [Symbol] name the name of the scope.
127
- # @param [Hash] parameters the query that this named scope will perform.
128
- #
129
- def named_scope(name, parameters)
130
- scope_proxy.add_named(name, parameters)
131
- end
132
-
133
- # Returns boolean based on whether the Document has a scope by a particular name.
134
- #
135
- # @param [Symbol] name The name of the scope in question.
136
- #
137
- def has_named_scope?(name)
138
- scope_proxy.has_named_scope?(name)
139
- end
140
-
141
- # Create an ad hoc scope on this Document.
142
- #
143
- # e.g.
144
- #
145
- # scope = Post.scope(:order! => :created_at)
146
- # scope.all # => [#<Post>, #<Post>]
147
- #
148
- # @param [Hash] parameters the query parameters to create the scope with.
149
- #
150
- def scope(parameters)
151
- scope_proxy.ad_hoc(parameters)
152
- end
153
-
154
- def association_set
155
- @association_set ||= Associations::Set.new(self)
156
- end
157
-
158
- # Add a has_many association.
159
- #
160
- # e.g.
161
- #
162
- # class Post
163
- # attribute :user_id, Friendly::UUID
164
- # indexes :user_id
165
- # end
166
- #
167
- # class User
168
- # has_many :posts
169
- # end
170
- #
171
- # @user = User.create
172
- # @post = @user.posts.create
173
- # @user.posts.all == [@post] # => true
174
- #
175
- # _Note: Make sure that the target model is indexed on the foreign key. If it isn't, querying the association will raise Friendly::MissingIndex._
176
- #
177
- # Friendly defaults the foreign key to class_name_id just like ActiveRecord.
178
- # It also converts the name of the association to the name of the target class just like ActiveRecord does.
179
- #
180
- # The biggest difference in semantics between Friendly's has_many and active_record's is that Friendly's just returns a Friendly::Scope object. If you want all the associated objects, you have to call #all to get them. You can also use any other Friendly::Scope method.
181
- #
182
- # @param [Symbol] name The name of the association and plural name of the target class.
183
- # @option options [String] :class_name The name of the target class of this association if it is different than the name would imply.
184
- # @option options [Symbol] :foreign_key Override the foreign key.
185
- #
186
- def has_many(name, options = {})
187
- association_set.add(name, options)
188
- end
189
-
190
- protected
191
- def query(conditions)
192
- conditions.is_a?(Query) ? conditions : query_klass.new(conditions)
193
- end
194
38
  end
195
39
 
196
- def initialize(opts = {})
197
- self.attributes = opts
198
- end
199
-
200
- def attributes=(attrs)
201
- assert_no_duplicate_keys(attrs)
202
- attrs.each { |name, value| send("#{name}=", value) }
203
- end
204
-
205
- def save
206
- new_record? ? storage_proxy.create(self) : storage_proxy.update(self)
207
- end
208
-
209
- def update_attributes(attributes)
210
- self.attributes = attributes
211
- save
212
- end
213
-
214
- def destroy
215
- storage_proxy.destroy(self)
216
- end
217
-
218
- def to_hash
219
- Hash[*self.class.attributes.keys.map { |n| [n, send(n)] }.flatten]
220
- end
40
+ include Associations
41
+ include Convenience
42
+ include Scoping
43
+ include Storage
44
+ include Attributes
221
45
 
222
46
  def table_name
223
47
  self.class.table_name
@@ -236,22 +60,11 @@ module Friendly
236
60
  @new_record = value
237
61
  end
238
62
 
239
- def storage_proxy
240
- self.class.storage_proxy
241
- end
242
-
243
63
  def ==(comparison_object)
244
64
  comparison_object.equal?(self) ||
245
65
  (comparison_object.is_a?(self.class) &&
246
66
  !comparison_object.new_record? &&
247
67
  comparison_object.id == id)
248
68
  end
249
-
250
- protected
251
- def assert_no_duplicate_keys(hash)
252
- if hash.keys.map { |k| k.to_s }.uniq.length < hash.keys.length
253
- raise ArgumentError, "Duplicate keys: #{hash.inspect}"
254
- end
255
- end
256
69
  end
257
70
  end
@@ -0,0 +1,50 @@
1
+ require 'friendly/associations'
2
+ require 'friendly/document/mixin'
3
+
4
+ module Friendly
5
+ module Document
6
+ module Associations
7
+ extend Mixin
8
+
9
+ module ClassMethods
10
+ attr_writer :association_set
11
+
12
+ def association_set
13
+ @association_set ||= Friendly::Associations::Set.new(self)
14
+ end
15
+
16
+ # Add a has_many association.
17
+ #
18
+ # e.g.
19
+ #
20
+ # class Post
21
+ # attribute :user_id, Friendly::UUID
22
+ # indexes :user_id
23
+ # end
24
+ #
25
+ # class User
26
+ # has_many :posts
27
+ # end
28
+ #
29
+ # @user = User.create
30
+ # @post = @user.posts.create
31
+ # @user.posts.all == [@post] # => true
32
+ #
33
+ # _Note: Make sure that the target model is indexed on the foreign key. If it isn't, querying the association will raise Friendly::MissingIndex._
34
+ #
35
+ # Friendly defaults the foreign key to class_name_id just like ActiveRecord.
36
+ # It also converts the name of the association to the name of the target class just like ActiveRecord does.
37
+ #
38
+ # The biggest difference in semantics between Friendly's has_many and active_record's is that Friendly's just returns a Friendly::Scope object. If you want all the associated objects, you have to call #all to get them. You can also use any other Friendly::Scope method.
39
+ #
40
+ # @param [Symbol] name The name of the association and plural name of the target class.
41
+ # @option options [String] :class_name The name of the target class of this association if it is different than the name would imply.
42
+ # @option options [Symbol] :foreign_key Override the foreign key.
43
+ #
44
+ def has_many(name, options = {})
45
+ association_set.add(name, options)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,114 @@
1
+ require 'friendly/document/mixin'
2
+ require 'set'
3
+
4
+ module Friendly
5
+ module Document
6
+ module Attributes
7
+ extend Mixin
8
+
9
+ module ClassMethods
10
+ def attribute(name, type = nil, options = {})
11
+ attributes[name] = Attribute.new(self, name, type, options)
12
+ end
13
+
14
+ def attributes
15
+ @attributes ||= {}
16
+ end
17
+
18
+ def new_without_change_tracking(attributes)
19
+ doc = new(attributes)
20
+ doc.reset_changes
21
+ doc
22
+ end
23
+ end
24
+
25
+ def initialize(opts = {})
26
+ assign_default_values
27
+ self.attributes = opts
28
+ end
29
+
30
+ def attributes=(attrs)
31
+ assert_no_duplicate_keys(attrs)
32
+ attrs.each { |name, value| assign(name, value) }
33
+ end
34
+
35
+ def to_hash
36
+ Hash[*self.class.attributes.keys.map { |n| [n, send(n)] }.flatten]
37
+ end
38
+
39
+ def assign_default_values
40
+ self.class.attributes.values.each { |a| a.assign_default_value(self) }
41
+ end
42
+
43
+ def assign(name, value)
44
+ send(:"#{name}=", value)
45
+ end
46
+
47
+ # Notify the object that an attribute is about to change.
48
+ #
49
+ # @param [Symbol] attribute The name of the attribute about to change.
50
+ #
51
+ def will_change(attribute)
52
+ changed << attribute
53
+ instance_variable_set(:"@#{attribute}_was", send(attribute))
54
+ end
55
+
56
+ # Get the original value of an attribute that has changed.
57
+ #
58
+ # @param [Symbol] attribute The name of the attribute.
59
+ #
60
+ def attribute_was(attribute)
61
+ instance_variable_get(:"@#{attribute}_was")
62
+ end
63
+
64
+ # Has this attribute changed?
65
+ #
66
+ # @param [Symbol] attribute The name of the attribute.
67
+ #
68
+ def attribute_changed?(attribute)
69
+ changed.include?(attribute)
70
+ end
71
+
72
+ # Have any of the attributes that are being tracked changed since last reset?
73
+ #
74
+ def changed?
75
+ !changed.empty?
76
+ end
77
+
78
+ # Which attributes that are being tracked have changed since last reset?
79
+ #
80
+ def changed
81
+ @changed ||= Set.new
82
+ end
83
+
84
+ # Reset all the changes to this object.
85
+ #
86
+ def reset_changes
87
+ changed.each { |c| not_changed(c) }.clear
88
+ end
89
+
90
+ # Reset the changed-ness of one attribute.
91
+ #
92
+ def not_changed(attribute)
93
+ instance_variable_set(:"@#{attribute}_was", nil)
94
+ changed.delete(attribute)
95
+ end
96
+
97
+ # Override #save to reset changes afterwards
98
+ #
99
+ # @override
100
+ #
101
+ def save
102
+ super
103
+ reset_changes
104
+ end
105
+
106
+ protected
107
+ def assert_no_duplicate_keys(hash)
108
+ if hash.keys.map { |k| k.to_s }.uniq.length < hash.keys.length
109
+ raise ArgumentError, "Duplicate keys: #{hash.inspect}"
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,41 @@
1
+ require 'friendly/document/mixin'
2
+
3
+ module Friendly
4
+ module Document
5
+ module Convenience
6
+ extend Mixin
7
+
8
+ module ClassMethods
9
+ attr_writer :collection_klass
10
+
11
+ def collection_klass
12
+ @collection_klass ||= WillPaginate::Collection
13
+ end
14
+
15
+ def find(id)
16
+ doc = first(:id => id)
17
+ raise RecordNotFound, "Couldn't find #{name}/#{id}" if doc.nil?
18
+ doc
19
+ end
20
+
21
+ def paginate(conditions)
22
+ query = query(conditions)
23
+ count = count(query)
24
+ collection = collection_klass.new(query.page, query.per_page, count)
25
+ collection.replace(all(query))
26
+ end
27
+
28
+ def create(attributes = {})
29
+ doc = new(attributes)
30
+ doc.save
31
+ doc
32
+ end
33
+ end
34
+
35
+ def update_attributes(attributes)
36
+ self.attributes = attributes
37
+ save
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module Friendly
2
+ module Document
3
+ module Mixin
4
+ # FIXME: I'm not in love with this. But, I also don't think it's the
5
+ # end of the world.
6
+ def included(klass)
7
+ if klass.const_defined?(:ClassMethods)
8
+ klass.const_get(:ClassMethods).send(:include, const_get(:ClassMethods))
9
+ else
10
+ klass.send(:extend, const_get(:ClassMethods))
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,66 @@
1
+ require 'friendly/document/mixin'
2
+
3
+ module Friendly
4
+ module Document
5
+ module Scoping
6
+ extend Mixin
7
+
8
+ module ClassMethods
9
+ attr_writer :scope_proxy
10
+
11
+ def scope_proxy
12
+ @scope_proxy ||= ScopeProxy.new(self)
13
+ end
14
+
15
+ # Add a named scope to this Document.
16
+ #
17
+ # e.g.
18
+ #
19
+ # class Post
20
+ # indexes :created_at
21
+ # named_scope :recent, :order! => :created_at.desc
22
+ # end
23
+ #
24
+ # Then, you can access the recent posts with:
25
+ #
26
+ # Post.recent.all
27
+ # ...or...
28
+ # Post.recent.first
29
+ #
30
+ # Both #all and #first also take additional parameters:
31
+ #
32
+ # Post.recent.all(:author_id => @author.id)
33
+ #
34
+ # Scopes are also chainable. See the README or Friendly::Scope docs for details.
35
+ #
36
+ # @param [Symbol] name the name of the scope.
37
+ # @param [Hash] parameters the query that this named scope will perform.
38
+ #
39
+ def named_scope(name, parameters)
40
+ scope_proxy.add_named(name, parameters)
41
+ end
42
+
43
+ # Returns boolean based on whether the Document has a scope by a particular name.
44
+ #
45
+ # @param [Symbol] name The name of the scope in question.
46
+ #
47
+ def has_named_scope?(name)
48
+ scope_proxy.has_named_scope?(name)
49
+ end
50
+
51
+ # Create an ad hoc scope on this Document.
52
+ #
53
+ # e.g.
54
+ #
55
+ # scope = Post.scope(:order! => :created_at)
56
+ # scope.all # => [#<Post>, #<Post>]
57
+ #
58
+ # @param [Hash] parameters the query parameters to create the scope with.
59
+ #
60
+ def scope(parameters)
61
+ scope_proxy.ad_hoc(parameters)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,63 @@
1
+ require 'friendly/document/mixin'
2
+
3
+ module Friendly
4
+ module Document
5
+ module Storage
6
+ extend Mixin
7
+
8
+ module ClassMethods
9
+ attr_writer :storage_proxy, :query_klass
10
+
11
+ def create_tables!
12
+ storage_proxy.create_tables!
13
+ end
14
+
15
+ def storage_proxy
16
+ @storage_proxy ||= StorageProxy.new(self)
17
+ end
18
+
19
+ def indexes(*args)
20
+ storage_proxy.add(args)
21
+ end
22
+
23
+ def caches_by(*fields)
24
+ options = fields.last.is_a?(Hash) ? fields.pop : {}
25
+ storage_proxy.cache(fields, options)
26
+ end
27
+
28
+ def first(query)
29
+ storage_proxy.first(query(query))
30
+ end
31
+
32
+ def all(query)
33
+ storage_proxy.all(query(query))
34
+ end
35
+
36
+ def count(conditions)
37
+ storage_proxy.count(query(conditions))
38
+ end
39
+
40
+ def query_klass
41
+ @query_klass ||= Query
42
+ end
43
+
44
+ protected
45
+ def query(conditions)
46
+ conditions.is_a?(Query) ? conditions : query_klass.new(conditions)
47
+ end
48
+ end
49
+
50
+ def save
51
+ new_record? ? storage_proxy.create(self) : storage_proxy.update(self)
52
+ end
53
+
54
+ def destroy
55
+ storage_proxy.destroy(self)
56
+ end
57
+
58
+ def storage_proxy
59
+ self.class.storage_proxy
60
+ end
61
+ end
62
+ end
63
+ end
@@ -12,7 +12,8 @@ module Friendly
12
12
  def to_object(klass, record)
13
13
  record.delete(:added_id)
14
14
  attributes = serializer.parse(record.delete(:attributes))
15
- klass.new attributes.merge(record).merge(:new_record => false)
15
+ attributes.merge!(record).merge!(:new_record => false)
16
+ klass.new_without_change_tracking attributes
16
17
  end
17
18
 
18
19
  def to_record(document)
@@ -1,15 +1,30 @@
1
1
  require File.expand_path("../../spec_helper", __FILE__)
2
2
 
3
3
  describe "An attribute with a default value" do
4
- before do
5
- @user = User.new
6
- end
4
+ describe "before saving" do
5
+ before do
6
+ @user = User.new
7
+ end
8
+
9
+ it "has the value by default" do
10
+ @user.happy.should be_true
11
+ end
7
12
 
8
- it "has the value by default" do
9
- @user.happy.should be_true
13
+ it "has a default vaue even when it's false" do
14
+ @user.sad.should be_false
15
+ end
10
16
  end
11
17
 
12
- it "has a default vaue even when it's false" do
13
- @user.sad.should be_false
18
+ describe "after saving" do
19
+ before do
20
+ @user = User.new
21
+ @user.save
22
+ @user = User.find(@user.id)
23
+ end
24
+
25
+ it "doesn't set the existing attributes as dirty" do
26
+ @user.should_not be_changed
27
+ @user.should_not be_happy_changed
28
+ end
14
29
  end
15
30
  end
@@ -0,0 +1,43 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ describe "Changing an attribute" do
4
+ describe "before a record is saved" do
5
+ before do
6
+ @user = User.new
7
+ @user.name = "James"
8
+ end
9
+
10
+ it "responds to the attribute being changed" do
11
+ @user.should be_name_changed
12
+ end
13
+
14
+ it "returns the original value of the attribute" do
15
+ @user.name_was.should == ""
16
+ end
17
+
18
+ it "is changed" do
19
+ @user.should be_changed
20
+ end
21
+ end
22
+
23
+ describe "after saving a record with changed attributes" do
24
+ before do
25
+ @user = User.create
26
+ @user.name = "James"
27
+ @user.save
28
+ end
29
+
30
+ it "is no longer attribute_changed?" do
31
+ @user.should_not be_name_changed
32
+ end
33
+
34
+ it "returns nil for attribute_was" do
35
+ @user.name_was.should be_nil
36
+ end
37
+
38
+ it "is no longer changed" do
39
+ @user.should_not be_changed
40
+ end
41
+ end
42
+ end
43
+
@@ -62,3 +62,10 @@ describe "limiting a query with offset" do
62
62
  :order! => :created_at.desc).should == @objects.reverse.slice(2, 2)
63
63
  end
64
64
  end
65
+
66
+ describe "all with only order" do
67
+ it "queries the index" do
68
+ Address.create
69
+ Address.all(:order! => :created_at.desc, :limit! => 5)
70
+ end
71
+ end
@@ -64,6 +64,8 @@ class Address
64
64
 
65
65
  indexes :user_id
66
66
  indexes :street
67
+ indexes :created_at
68
+
67
69
  caches_by :id
68
70
  end
69
71
 
@@ -2,12 +2,12 @@ require File.expand_path("../../spec_helper", __FILE__)
2
2
 
3
3
  describe "Friendly::Attribute" do
4
4
  before do
5
- @klass = Class.new
5
+ @klass = Class.new { def will_change(a); end }
6
6
  @name = Friendly::Attribute.new(@klass, :name, String)
7
7
  @id = Friendly::Attribute.new(@klass, :id, Friendly::UUID)
8
8
  @no_type = Friendly::Attribute.new(@klass, :no_type, nil)
9
9
  @default = Friendly::Attribute.new(@klass, :default, String, :default => "asdf")
10
- @false = Friendly::Attribute.new(@klass, :default, String, :default => false)
10
+ @false = Friendly::Attribute.new(@klass, :false, String, :default => false)
11
11
  @klass.stubs(:attributes).returns({:name => @name,
12
12
  :id => @id,
13
13
  :default => @default,
@@ -15,11 +15,27 @@ describe "Friendly::Attribute" do
15
15
  @object = @klass.new
16
16
  end
17
17
 
18
- it "creates a setter and a getter on klass" do
18
+ it "creates a getting on klass that notifies it of a change" do
19
+ @object.stubs(:will_change)
20
+ @object.name = "Something"
21
+ @object.should have_received(:will_change).with(:name)
22
+ end
23
+
24
+ it "creates a getter on klass" do
19
25
  @object.name = "Something"
20
26
  @object.name.should == "Something"
21
27
  end
22
28
 
29
+ it "creates an 'attr_was' getter" do
30
+ @object.instance_variable_set(:@name_was, "Joe the Plumber")
31
+ @object.name_was.should == "Joe the Plumber"
32
+ end
33
+
34
+ it "creates an attr_changed? query method" do
35
+ @object.stubs(:attribute_changed?).with(:name).returns(true)
36
+ @object.should be_name_changed
37
+ end
38
+
23
39
  it "typecasts values using the converter function" do
24
40
  uuid = Friendly::UUID.new
25
41
  @id.typecast(uuid.to_s).should == uuid
@@ -37,10 +53,6 @@ describe "Friendly::Attribute" do
37
53
  }.should raise_error(Friendly::NoConverterExists)
38
54
  end
39
55
 
40
- it "creates a getter with a default value" do
41
- @object.id.should be_instance_of(Friendly::UUID)
42
- end
43
-
44
56
  it "has a default value of type.new" do
45
57
  @id.default.should be_instance_of(Friendly::UUID)
46
58
  end
@@ -55,13 +67,21 @@ describe "Friendly::Attribute" do
55
67
 
56
68
  it "can have a default value" do
57
69
  @default.default.should == "asdf"
58
- @klass.new.default.should == "asdf"
70
+ @obj = @klass.new
71
+ @default.assign_default_value(@obj)
72
+ @obj.default.should == "asdf"
59
73
  end
60
74
 
61
75
  it "has a default value even if it's false" do
62
76
  @false.default.should be_false
63
77
  end
64
78
 
79
+ it "knows how to assign its own default" do
80
+ @object = stub(:false= => nil)
81
+ @false.assign_default_value(@object)
82
+ @object.should have_received(:false=).with(false)
83
+ end
84
+
65
85
  describe "registering a type" do
66
86
  before do
67
87
  @klass = Class.new
@@ -76,6 +76,19 @@ describe "Friendly::DataStore" do
76
76
  end
77
77
  end
78
78
 
79
+ describe "all without conditions" do
80
+ before do
81
+ @all = stub(:map => [])
82
+ @users.stubs(:order).with(:created_at).returns(@all)
83
+ @query = query(:order! => :created_at)
84
+ @return = @datastore.all(@klass, @query)
85
+ end
86
+
87
+ it "orders the filtered dataset and returns the results" do
88
+ @return.should == []
89
+ end
90
+ end
91
+
79
92
  describe "retrieving first with conditions" do
80
93
  before do
81
94
  @users.first = {{:id => 1} => {:id => 1}}
@@ -0,0 +1,130 @@
1
+ require File.expand_path("../../../spec_helper", __FILE__)
2
+
3
+ describe "Friendly::Document::Attributes" do
4
+ before do
5
+ @klass = Class.new do
6
+ include Friendly::Document::Attributes
7
+
8
+ attribute :name, String
9
+ end
10
+ end
11
+
12
+ describe "#initialize" do
13
+ it "sets the attributes using the setters" do
14
+ @doc = @klass.new :name => "Bond"
15
+ @doc.name.should == "Bond"
16
+ end
17
+
18
+ it "assigns the default values" do
19
+ @klass.attribute :id, Friendly::UUID
20
+ @klass.attributes[:id] = stub(:assign_default_value => nil)
21
+ @klass.attributes[:name] = stub(:assign_default_value => nil,
22
+ :typecast => "Bond")
23
+ @doc = @klass.new :name => "Bond"
24
+ @klass.attributes[:id].should have_received(:assign_default_value).with(@doc)
25
+ @klass.attributes[:name].should have_received(:assign_default_value).with(@doc)
26
+ end
27
+ end
28
+
29
+ describe "#attributes=" do
30
+ before do
31
+ @object = @klass.new
32
+ @object.attributes = {:name => "Bond"}
33
+ end
34
+
35
+ it "sets the attributes using the setters" do
36
+ @object.name.should == "Bond"
37
+ end
38
+
39
+ it "raises ArgumentError when there are duplicate keys of differing type" do
40
+ lambda {
41
+ @object.attributes = {:name => "Bond", "name" => "Bond"}
42
+ }.should raise_error(ArgumentError)
43
+ end
44
+ end
45
+
46
+ describe "#to_hash" do
47
+ before do
48
+ @object = @klass.new(:name => "Stewie")
49
+ end
50
+
51
+ it "creates a hash that contains its attributes" do
52
+ @object.to_hash.should == {:name => "Stewie"}
53
+ end
54
+ end
55
+
56
+ describe "#assign" do
57
+ before do
58
+ @object = @klass.new
59
+ @object.assign(:name, "James Bond")
60
+ end
61
+
62
+ it "assigns the value to the attribute" do
63
+ @object.name.should == "James Bond"
64
+ end
65
+ end
66
+
67
+ describe "#will_change" do
68
+ before do
69
+ @klass.send(:attr_accessor, :some_variable)
70
+ @object = @klass.new
71
+ @object.some_variable = "Some value"
72
+ @object.will_change(:some_variable)
73
+ end
74
+
75
+ it "makes the object #changed?" do
76
+ @object.should be_changed
77
+ end
78
+
79
+ it "returns the value of the variable for #attribute_was" do
80
+ @object.attribute_was(:some_variable).should == "Some value"
81
+ end
82
+
83
+ it "returns true for attribute_changed?(:some_variable)" do
84
+ @object.should be_attribute_changed(:some_variable)
85
+ end
86
+ end
87
+
88
+ describe "#reset_changes" do
89
+ before do
90
+ @klass.send(:attr_accessor, :some_variable)
91
+ @object = @klass.new
92
+ @object.some_variable = "Some value"
93
+ @object.will_change(:some_variable)
94
+ @object.reset_changes
95
+ end
96
+
97
+ it "resets the changed status of the object" do
98
+ @object.should_not be_changed
99
+ end
100
+
101
+ it "returns nil for attribute_was(:some_variable)" do
102
+ @object.attribute_was(:some_variable).should be_nil
103
+ end
104
+
105
+ it "returns false for attribute_changed?(:some_variable)" do
106
+ @object.should_not be_attribute_changed(:some_variable)
107
+ end
108
+ end
109
+
110
+ describe "#new_without_change_tracking" do
111
+ before do
112
+ @klass = Class.new do
113
+ attr_reader :name
114
+
115
+ def name=(name)
116
+ will_change(:name)
117
+ @name = name
118
+ end
119
+
120
+ include Friendly::Document::Attributes
121
+ end
122
+ @doc = @klass.new_without_change_tracking(:name => "James")
123
+ end
124
+
125
+ it "initializes and then calls reset_changes" do
126
+ @doc.name.should == "James"
127
+ @doc.should_not be_changed
128
+ end
129
+ end
130
+ end
@@ -60,46 +60,6 @@ describe "Friendly::Document" do
60
60
  end
61
61
  end
62
62
 
63
- describe "converting a document to a hash" do
64
- before do
65
- @object = @klass.new(:name => "Stewie")
66
- end
67
-
68
- it "creates a hash that contains its attributes" do
69
- @object.to_hash.should == {:name => "Stewie",
70
- :id => @object.id,
71
- :created_at => @object.created_at,
72
- :updated_at => @object.updated_at}
73
- end
74
- end
75
-
76
- describe "setting the attributes all at once" do
77
- before do
78
- @object = @klass.new
79
- @object.attributes = {:name => "Bond"}
80
- end
81
-
82
- it "sets the attributes using the setters" do
83
- @object.name.should == "Bond"
84
- end
85
-
86
- it "raises ArgumentError when there are duplicate keys of differing type" do
87
- lambda {
88
- @object.attributes = {:name => "Bond", "name" => "Bond"}
89
- }.should raise_error(ArgumentError)
90
- end
91
- end
92
-
93
- describe "initializing a document" do
94
- before do
95
- @doc = @klass.new :name => "Bond"
96
- end
97
-
98
- it "sets the attributes using the setters" do
99
- @doc.name.should == "Bond"
100
- end
101
- end
102
-
103
63
  describe "table name" do
104
64
  it "by default: is the class name, converted with pluralize.underscore" do
105
65
  User.table_name.should == "users"
@@ -16,20 +16,15 @@ describe "Friendly::Translator" do
16
16
  :created_at => @time,
17
17
  :updated_at => @time,
18
18
  :attributes => "THE JSON"}
19
- @klass = FakeDocument
20
- @doc = @translator.to_object(@klass, @row)
19
+ @doc = stub
20
+ @klass = stub
21
+ @klass.stubs(:new_without_change_tracking).
22
+ with(:updated_at => @time, :new_record => false,
23
+ :name => "Stewie", :created_at => @time).returns(@doc)
21
24
  end
22
25
 
23
- it "creates a klass with the attributes from the json" do
24
- @doc.name.should == "Stewie"
25
- end
26
-
27
- it "sets updated_at" do
28
- @doc.updated_at.should == @time
29
- end
30
-
31
- it "sets new_record to false" do
32
- @doc.new_record.should be_false
26
+ it "creates a new object without change tracking" do
27
+ @translator.to_object(@klass, @row).should == @doc
33
28
  end
34
29
  end
35
30
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: friendly
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Golick
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-24 00:00:00 -06:00
12
+ date: 2010-01-16 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -122,13 +122,17 @@ files:
122
122
  - lib/friendly/boolean.rb
123
123
  - lib/friendly/cache.rb
124
124
  - lib/friendly/cache/by_id.rb
125
- - lib/friendly/config.rb
126
125
  - lib/friendly/data_store.rb
127
126
  - lib/friendly/document.rb
127
+ - lib/friendly/document/associations.rb
128
+ - lib/friendly/document/attributes.rb
129
+ - lib/friendly/document/convenience.rb
130
+ - lib/friendly/document/mixin.rb
131
+ - lib/friendly/document/scoping.rb
132
+ - lib/friendly/document/storage.rb
128
133
  - lib/friendly/document_table.rb
129
134
  - lib/friendly/index.rb
130
135
  - lib/friendly/memcached.rb
131
- - lib/friendly/named_scope.rb
132
136
  - lib/friendly/newrelic.rb
133
137
  - lib/friendly/query.rb
134
138
  - lib/friendly/scope.rb
@@ -156,6 +160,7 @@ files:
156
160
  - spec/integration/convenience_api_spec.rb
157
161
  - spec/integration/count_spec.rb
158
162
  - spec/integration/default_value_spec.rb
163
+ - spec/integration/dirty_tracking_spec.rb
159
164
  - spec/integration/find_via_cache_spec.rb
160
165
  - spec/integration/finder_spec.rb
161
166
  - spec/integration/has_many_spec.rb
@@ -172,14 +177,13 @@ files:
172
177
  - spec/unit/attribute_spec.rb
173
178
  - spec/unit/cache_by_id_spec.rb
174
179
  - spec/unit/cache_spec.rb
175
- - spec/unit/config_spec.rb
176
180
  - spec/unit/data_store_spec.rb
181
+ - spec/unit/document/attributes_spec.rb
177
182
  - spec/unit/document_spec.rb
178
183
  - spec/unit/document_table_spec.rb
179
184
  - spec/unit/friendly_spec.rb
180
185
  - spec/unit/index_spec.rb
181
186
  - spec/unit/memcached_spec.rb
182
- - spec/unit/named_scope_spec.rb
183
187
  - spec/unit/query_spec.rb
184
188
  - spec/unit/scope_proxy_spec.rb
185
189
  - spec/unit/scope_spec.rb
@@ -271,6 +275,7 @@ test_files:
271
275
  - spec/integration/convenience_api_spec.rb
272
276
  - spec/integration/count_spec.rb
273
277
  - spec/integration/default_value_spec.rb
278
+ - spec/integration/dirty_tracking_spec.rb
274
279
  - spec/integration/find_via_cache_spec.rb
275
280
  - spec/integration/finder_spec.rb
276
281
  - spec/integration/has_many_spec.rb
@@ -286,14 +291,13 @@ test_files:
286
291
  - spec/unit/attribute_spec.rb
287
292
  - spec/unit/cache_by_id_spec.rb
288
293
  - spec/unit/cache_spec.rb
289
- - spec/unit/config_spec.rb
290
294
  - spec/unit/data_store_spec.rb
295
+ - spec/unit/document/attributes_spec.rb
291
296
  - spec/unit/document_spec.rb
292
297
  - spec/unit/document_table_spec.rb
293
298
  - spec/unit/friendly_spec.rb
294
299
  - spec/unit/index_spec.rb
295
300
  - spec/unit/memcached_spec.rb
296
- - spec/unit/named_scope_spec.rb
297
301
  - spec/unit/query_spec.rb
298
302
  - spec/unit/scope_proxy_spec.rb
299
303
  - spec/unit/scope_spec.rb
@@ -1,5 +0,0 @@
1
- module Friendly
2
- class Config < Struct.new(:repository)
3
- end
4
- end
5
-
@@ -1,17 +0,0 @@
1
- require 'friendly/scope'
2
-
3
- module Friendly
4
- class NamedScope
5
- attr_reader :klass, :parameters, :scope_klass
6
-
7
- def initialize(klass, parameters, scope_klass = Scope)
8
- @klass = klass
9
- @parameters = parameters
10
- @scope_klass = scope_klass
11
- end
12
-
13
- def scope
14
- @scope_klass.new(@klass, @parameters)
15
- end
16
- end
17
- end
@@ -1,4 +0,0 @@
1
- require File.expand_path("../../spec_helper", __FILE__)
2
-
3
- describe "Friendly::Config" do
4
- end
@@ -1,16 +0,0 @@
1
- require File.expand_path("../../spec_helper", __FILE__)
2
-
3
- describe "Friendly::NamedScope" do
4
- before do
5
- @klass = stub
6
- @scope = stub
7
- @scope_klass = stub
8
- @parameters = {:name => "James"}
9
- @scope_klass.stubs(:new).with(@klass, @parameters).returns(@scope)
10
- @named_scope = Friendly::NamedScope.new(@klass, @parameters, @scope_klass)
11
- end
12
-
13
- it "provides scope instances with the given parameters" do
14
- @named_scope.scope.should == @scope
15
- end
16
- end