friendly 0.4.3 → 0.4.4

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.
@@ -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