friendly_postgres 0.4.3 → 0.4.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -1,5 +1,3 @@
1
- require 'friendly/named_scope'
2
-
3
1
  module Friendly
4
2
  class ScopeProxy
5
3
  attr_reader :klass, :scope_klass, :scopes
@@ -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)
data/lib/friendly.rb CHANGED
@@ -3,13 +3,11 @@ require 'friendly/attribute'
3
3
  require 'friendly/boolean'
4
4
  require 'friendly/cache'
5
5
  require 'friendly/cache/by_id'
6
- require 'friendly/config'
7
6
  require 'friendly/data_store'
8
7
  require 'friendly/document'
9
8
  require 'friendly/document_table'
10
9
  require 'friendly/index'
11
10
  require 'friendly/memcached'
12
- require 'friendly/named_scope'
13
11
  require 'friendly/query'
14
12
  require 'friendly/sequel_monkey_patches'
15
13
  require 'friendly/scope'
@@ -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
data/spec/spec_helper.rb CHANGED
@@ -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