attr_remote 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ (The MIT License)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,69 @@
1
+ = attr_remote - Painlessly integrate ActiveResource into ActiveRecord.
2
+
3
+ == Why?
4
+
5
+ ActiveResource is cool, but it's is a pain to integrate with ActiveRecord. Want to have_many :my_active_resource? Nope. Also, you can't front any ActiveResource classes with a form. Want to use form_for? Sorry. Want validations? No dice.
6
+
7
+ == How does attr_remote help?
8
+
9
+ It allows you to hide an ActiveResource behind ActiveRecord, which means you can (sort of) have all the things you want, but can't with vanilla ActiveResource. Essentially, ActiveRecord becomes a proxy for ActiveResource.
10
+
11
+ == Okay, show me some code.
12
+
13
+ # attr_remote requires an integer attribute remote_<class>_id for
14
+ # the local ActiveRecord to store the ActiveResource ID
15
+ #
16
+ # For instance,
17
+ #
18
+ # create_table :users do |t|
19
+ # t.integer :remote_user_id
20
+ # t.integer :group_id
21
+ # end
22
+ class User
23
+ attr_remote :first_name, :last_name, :email, :password
24
+
25
+ belongs_to :group
26
+ has_many :posts
27
+
28
+
29
+ validates_length_of :first_name, :within => 1..30
30
+ validates_length_of :last_name, :within => 1..30
31
+ validates_presence_of :email
32
+ validates_presence_of :password, :on => :create
33
+ end
34
+
35
+ # The configuration behind the scenes:
36
+ #
37
+ # attr_remote assumes that there is an ActiveResource class
38
+ # with a Remote prefix
39
+ class RemoteUser < ActiveResource::Base
40
+ self.site = "https://myservice.com/"
41
+ self.element_name = "user"
42
+ end
43
+
44
+ With the above code, you can now CRUD (well, not delete yet) a User, and it will do all the right stuff behind the scenes with the remote resource. It even caches the remote instance the first time a remote attribute is read.
45
+
46
+ Want to use form_for? No problem. It works since you're really interacting with ActiveRecord and _not_ ActiveResource. Cool. Validations? Yep. Just don't do validates_uniqueness_of. :)
47
+
48
+ == Install
49
+
50
+ $ gem sources -a http://gems.github.com
51
+ $ sudo gem install codebrulee-attr_remote
52
+
53
+ == Any issues/TODOs?
54
+
55
+ * Delete isn't implemented yet.
56
+ * No support for validates_uniqueness_of.
57
+ * attr_remote declarations create a reader and writer. There isn't support for just reading or writing.
58
+ * Not all of the ActiveRecord methods do what you might expect. For instance, ActiveRecord::Base#reload doesn't delegate to ActiveResource yet, which means the locally cached instance isn't reloaded.
59
+ * Hook the caching into something like ActiveSupport::Cache::Store
60
+
61
+ == License
62
+
63
+ (The MIT License)
64
+
65
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
66
+
67
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
68
+
69
+ THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,31 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "attr_remote"
3
+ s.version = "0.0.7"
4
+ s.date = "2008-01-06"
5
+ s.summary = "Painlessly integrate ActiveResource into ActiveRecord."
6
+ s.email = "smithk14@gmail.com"
7
+ s.homepage = "http://github.com/codebrulee/attr_remote"
8
+ s.description = "Painlessly integrate ActiveResource into ActiveRecord."
9
+ s.has_rdoc = true
10
+ s.authors = ["Kevin Smith"]
11
+ s.files = [
12
+ "README.rdoc",
13
+ "attr_remote.gemspec",
14
+ "LICENSE",
15
+ "lib/attr_remote.rb"
16
+ ]
17
+
18
+ s.test_files = [
19
+ "test/test_attr_remote.rb",
20
+ "test/helper.rb",
21
+ "test/factories.rb",
22
+ "test/db/test.db",
23
+ "test/models/bob.rb",
24
+ "test/models/remote_bob.rb",
25
+ "test/models/user.rb",
26
+ "test/models/remote_user.rb",
27
+ ]
28
+
29
+ s.rdoc_options = ["--main", "README.rdoc"]
30
+ s.extra_rdoc_files = ["README.rdoc"]
31
+ end
@@ -0,0 +1,191 @@
1
+ require 'active_record'
2
+ require 'active_support'
3
+ require 'active_resource'
4
+
5
+ module AttrRemote
6
+ module ClassMethods
7
+
8
+ def remote_attributes
9
+ @remote_attributes ||= []
10
+ end
11
+
12
+ def attr_remote(*remote_attrs)
13
+ remote_class = "Remote#{self.to_s}"
14
+ remote_instance_meth = remote_class.underscore
15
+ remote_instance_id = remote_class.foreign_key
16
+
17
+ remote_attributes.concat(remote_attrs).uniq!
18
+
19
+ class_eval <<-remote_access, __FILE__, __LINE__+1
20
+ # def remote_user
21
+ # if @remote_user
22
+ # @remote_user
23
+ # elsif self.remote_user_id
24
+ # @remote_user = RemoteUser.find(self.remote_user_id) rescue nil
25
+ # else
26
+ # nil
27
+ # end
28
+ # end
29
+ def #{remote_instance_meth}
30
+ if @#{remote_instance_meth}
31
+ @#{remote_instance_meth}
32
+ elsif self.#{remote_instance_id}
33
+ @#{remote_instance_meth} = #{remote_class}.find(self.#{remote_instance_id}) rescue nil
34
+ else
35
+ nil
36
+ end
37
+ end
38
+
39
+ # before_create :create_remote_user
40
+ before_create :create_#{remote_instance_meth}
41
+
42
+ # def create_remote_user
43
+ # unless self.remote_user_id
44
+ # remote_hash = {}
45
+ # self.class.remote_attributes.each do |attr|
46
+ # remote_hash[attr.to_sym] = self.send(attr.to_sym)
47
+ # end
48
+ # remote_hash[:validate_only] = true if validate_only
49
+ # @remote_user = RemoteUser.create(remote_hash)
50
+ #
51
+ # unless @remote_user.valid?
52
+ # @remote_user.errors.each do |attr, err|
53
+ # errors.add(attr, err)
54
+ # end
55
+ # return false
56
+ # else
57
+ # self.remote_user_id = @remote_user.id
58
+ # end
59
+ # end
60
+ # end
61
+ def create_#{remote_instance_meth}
62
+ unless self.#{remote_instance_id}
63
+ remote_hash = {}
64
+ self.class.remote_attributes.each do |attr|
65
+ remote_hash[attr.to_sym] = self.send(attr.to_sym)
66
+ end
67
+ @#{remote_instance_meth} = #{remote_class}.create(remote_hash)
68
+
69
+ unless @#{remote_instance_meth}.valid?
70
+ @#{remote_instance_meth}.errors.each do |attr, err|
71
+ errors.add(attr, err)
72
+ end
73
+ return false
74
+ else
75
+ self.#{remote_instance_id} = @#{remote_instance_meth}.id
76
+ end
77
+ end
78
+ end
79
+ private :create_#{remote_instance_meth}
80
+
81
+ # validate_on_create :validate_remote_user_on_create
82
+ validate_on_create :validate_#{remote_instance_meth}_on_create
83
+
84
+ # def validate_remote_user_on_create; end
85
+ def validate_#{remote_instance_meth}_on_create; end
86
+
87
+ # before_update :update_remote_user
88
+ before_update :update_#{remote_instance_meth}
89
+
90
+ # def update_remote_user
91
+ # if self.remote_user_id and self.remote_attributes_changed?
92
+ # remote_hash = {}
93
+ # self.class.remote_attributes.each do |attr|
94
+ # remote_hash[attr.to_sym] = self.send(attr.to_sym)
95
+ # end
96
+ # remote_user.load(remote_hash)
97
+ # remote_user.save
98
+ #
99
+ # unless @#{remote_instance_meth}.valid?
100
+ # remote_user.errors.each do |attr, err|
101
+ # errors.add(attr, err)
102
+ # end
103
+ # return false
104
+ # else
105
+ # @remote_attributes_changed = false
106
+ # return true
107
+ # end
108
+ # end
109
+ # end
110
+ def update_#{remote_instance_meth}
111
+ if self.#{remote_instance_id} and self.remote_attributes_changed?
112
+ remote_hash = {}
113
+ self.class.remote_attributes.each do |attr|
114
+ remote_hash[attr.to_sym] = self.send(attr.to_sym)
115
+ end
116
+ #{remote_instance_meth}.load(remote_hash)
117
+ #{remote_instance_meth}.save
118
+
119
+ unless @#{remote_instance_meth}.valid?
120
+ #{remote_instance_meth}.errors.each do |attr, err|
121
+ errors.add(attr, err)
122
+ end
123
+ return false
124
+ else
125
+ @remote_attributes_changed = false
126
+ return true
127
+ end
128
+ end
129
+ end
130
+ private :update_#{remote_instance_meth}
131
+
132
+ # validate_on_update :validate_remote_user_on_update
133
+ validate_on_update :validate_#{remote_instance_meth}_on_update
134
+
135
+ # def validate_remote_user_on_update; end
136
+ def validate_#{remote_instance_meth}_on_update; end
137
+ remote_access
138
+
139
+ remote_attributes.each do |attr|
140
+ class_eval <<-remote_attribute, __FILE__, __LINE__+1
141
+ def #{attr} # def username
142
+ remote_#{attr} || '' # remote_username || ''
143
+ end # end
144
+
145
+ def remote_#{attr} # def remote_username
146
+ if @#{attr} # if @username
147
+ @#{attr} # @username
148
+ elsif self.#{remote_instance_meth} # elsif self.remote_user
149
+ @#{attr} = self. # @username = self.
150
+ #{remote_instance_meth}. # remote_user.
151
+ #{attr} rescue nil # username rescue nil
152
+ else # else
153
+ nil # nil
154
+ end # end
155
+ end # end
156
+
157
+ def #{attr}=(attr_value) # def username=(attr_value)
158
+ @#{attr} = attr_value # @username = attr_value
159
+ @remote_attributes_changed = true # @remote_attributes_changed = true
160
+ end # end
161
+ remote_attribute
162
+ end
163
+ end
164
+ end
165
+
166
+ module InstanceMethods
167
+ def self.included(base)
168
+ base.alias_method_chain :save, :dirty_remote
169
+ base.alias_method_chain :save!, :dirty_remote
170
+ end
171
+
172
+ def remote_attributes_changed?
173
+ @remote_attributes_changed == true
174
+ end
175
+
176
+ def save_with_dirty_remote(*args) #:nodoc:
177
+ if status = save_without_dirty_remote(*args)
178
+ @remote_attributes_changed = false
179
+ end
180
+ status
181
+ end
182
+ def save_with_dirty_remote!(*args) #:nodoc:
183
+ status = save_without_dirty_remote!(*args)
184
+ @remote_attributes_changed = false
185
+ status
186
+ end
187
+ end
188
+ end
189
+
190
+ ActiveRecord::Base.extend AttrRemote::ClassMethods
191
+ ActiveRecord::Base.send(:include, AttrRemote::InstanceMethods)
Binary file
@@ -0,0 +1,8 @@
1
+ Factory.sequence :remote_user_id do |n|
2
+ n
3
+ end
4
+
5
+ Factory.define :user do |u|
6
+ u.name "Kevin"
7
+ u.remote_user_id { Factory.next(:remote_user_id) }
8
+ end
@@ -0,0 +1,37 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+
4
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'attr_remote')
5
+ require 'active_resource/http_mock'
6
+
7
+ Dir['**/models/*.rb'].each do |model|
8
+ require model
9
+ end
10
+
11
+ require 'redgreen'
12
+ require 'context'
13
+ require 'factory_girl'
14
+
15
+ ActiveRecord::Base.establish_connection({
16
+ :adapter => 'sqlite3',
17
+ :dbfile => File.dirname(__FILE__) + '/db/test.db'
18
+ })
19
+
20
+ class Test::Unit::TestCase
21
+ def mock_user(user)
22
+ ActiveResource::HttpMock.respond_to do |mock|
23
+ mock.get "/users/#{user.remote_user_id}.xml", {}, {
24
+ :id => user.remote_user_id,
25
+ :name => user.name
26
+ }.to_xml(:root => "user")
27
+ mock.put "/users/#{user.remote_user_id}.xml", {}, '', 200, {}
28
+ end
29
+ end
30
+
31
+
32
+ def teardown
33
+ # cleanup all our test data
34
+ User.delete_all
35
+ Bob.delete_all
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ class Bob < ActiveRecord::Base
2
+ validates_presence_of :title
3
+
4
+ attr_remote :email, :foo
5
+
6
+ def validate_remote_bob_on_create
7
+ errors.add(:foo, "has already been taken") if foo == "taken"
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ class RemoteBob < ActiveResource::Base
2
+ self.site = "http://0.0.0.0:3000/"
3
+ self.element_name = "bob"
4
+ end
@@ -0,0 +1,4 @@
1
+ class RemoteUser < ActiveResource::Base
2
+ self.site = "http://0.0.0.0:3000/"
3
+ self.element_name = "user"
4
+ end
@@ -0,0 +1,4 @@
1
+ class User < ActiveRecord::Base
2
+ attr_remote :name
3
+ attr_remote :not_here
4
+ end
@@ -0,0 +1,144 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class TestAttrRemote < Test::Unit::TestCase
4
+ context "general specs" do
5
+ test "that attributes can be added multiple times" do
6
+ assert_equal User.remote_attributes, [:name, :not_here]
7
+ end
8
+
9
+ test "that a remote hit isn't attempted if the local remote id is nil" do
10
+ user = User.new
11
+ assert_nil user.remote_user_id
12
+ assert_nil user.remote_user
13
+ assert_equal "", user.name
14
+ end
15
+ end
16
+
17
+ context "remote object access" do
18
+ before do
19
+ mock_user(@user = Factory(:user, :name => "Kevin"))
20
+ end
21
+ test "that an instance should have a method for the remote object" do
22
+ assert @user.respond_to?(:remote_user)
23
+ end
24
+ test "that the remote object should be looked up based on the id" do
25
+ assert_equal "Kevin", @user.remote_user.name
26
+ end
27
+ end
28
+
29
+ context "reading" do
30
+ test "that an instance should have a read method for the attribute" do
31
+ user = Factory(:user)
32
+ assert user.respond_to?(:name)
33
+ end
34
+
35
+ test "that the read method returns the correct value" do
36
+ user = Factory(:user)
37
+ assert_equal "Kevin", user.name
38
+ end
39
+
40
+ test "that after first read, an instance variable contains the value" do
41
+ mock_user(user = Factory(:user))
42
+ expected_name = user.name
43
+
44
+ user = User.find(user.id)
45
+ assert_nil user.instance_variable_get('@name')
46
+ user.name
47
+ assert_equal "Kevin", user.instance_variable_get('@name')
48
+ end
49
+
50
+ test "that an attribute will be empty if the remote side does not exist" do
51
+ user = Factory.build(:user, :remote_user_id => 666, :name => nil)
52
+ assert_equal '', user.name
53
+ end
54
+
55
+ test "that an attribute that is not readable returns an empty string" do
56
+ user = Factory(:user)
57
+ user = User.find(user.id)
58
+ assert_equal '', user.not_here
59
+ end
60
+ end
61
+
62
+ context "creation" do
63
+ test "that an instance should have a writer method for the attribute" do
64
+ user = Factory(:user)
65
+ assert user.respond_to?(:name=)
66
+ end
67
+ test "that an instance reads correctly after writing" do
68
+ user = Factory.build(:user, :name => "test")
69
+ assert_equal "test", user.name
70
+ end
71
+ test "that the remote object id is set after creation" do
72
+ user = Factory.build(:user, :remote_user_id => nil)
73
+ ActiveResource::HttpMock.respond_to do |mock|
74
+ mock.post "/users.xml", {}, {
75
+ :name => user.name
76
+ }.to_xml(:root => "user"), 200, {
77
+ 'Location' => 'http://0.0.0.0:3000/users/1'
78
+ }
79
+ end
80
+ assert user.save
81
+ assert_not_nil user.remote_user_id
82
+ end
83
+
84
+ test "that remote errors halt save process" do
85
+ bob = Bob.new(:title => "title")
86
+ assert !bob.save
87
+ assert_not_nil bob.errors.on(:email)
88
+ end
89
+
90
+ test "that failure of local validations halts remote creation and therefore remote errors do not show up" do
91
+ ActiveResource::HttpMock.respond_to do |mock|
92
+ mock.post "/bobs.xml",
93
+ {},
94
+ returning(ActiveRecord::Errors.new(Bob.new)) { |errors|
95
+ errors.add(:email, "can't be blank")
96
+ }.to_xml, 422
97
+ end
98
+ bob = Bob.create(:title => nil)
99
+ assert !bob.save
100
+ assert_not_nil bob.errors.on(:title)
101
+ assert_nil bob.errors.on(:email)
102
+ end
103
+
104
+ test "that after a successful creation an instance is marked as not having any remote changes" do
105
+ user = Factory(:user, :remote_user_id => 2)
106
+ assert !user.remote_attributes_changed?
107
+ end
108
+ end
109
+
110
+ context "updates" do
111
+ test "that a remote attribute change is propogated" do
112
+ mock_user(user = Factory(:user))
113
+ user.name = "boo"
114
+ assert user.save
115
+ mock_user(user)
116
+ user = User.find(user.id)
117
+ assert_equal "boo", user.name
118
+ end
119
+
120
+ test "that after a successful update an instance is marked as not having any remote changes" do
121
+ user = Factory(:user)
122
+ mock_user(user)
123
+ user.name = "new name"
124
+ assert user.save
125
+ assert !user.remote_attributes_changed?
126
+ end
127
+
128
+ test "that save doesn't cause a remote hit if a remote attribute has not been changed" do
129
+ user = Factory(:user, :remote_user_id => 2)
130
+ # purposely not mocking the ARes call to "prove"
131
+ # that it isn't invoked on save
132
+ assert user.save
133
+ end
134
+
135
+ test "that changing a local attribute does not trigger a remote save" do
136
+ bob = Bob.new(:remote_bob_id => 3)
137
+ bob.title = "test"
138
+ assert bob.save
139
+ end
140
+ end
141
+
142
+ context "deletes" do
143
+ end
144
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attr_remote
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.7
5
+ platform: ruby
6
+ authors:
7
+ - Kevin Smith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-01-06 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Painlessly integrate ActiveResource into ActiveRecord.
17
+ email: smithk14@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - README.rdoc
26
+ - attr_remote.gemspec
27
+ - LICENSE
28
+ - lib/attr_remote.rb
29
+ has_rdoc: true
30
+ homepage: http://github.com/codebrulee/attr_remote
31
+ licenses: []
32
+
33
+ post_install_message:
34
+ rdoc_options:
35
+ - --main
36
+ - README.rdoc
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ requirements: []
52
+
53
+ rubyforge_project:
54
+ rubygems_version: 1.3.5
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: Painlessly integrate ActiveResource into ActiveRecord.
58
+ test_files:
59
+ - test/test_attr_remote.rb
60
+ - test/helper.rb
61
+ - test/factories.rb
62
+ - test/db/test.db
63
+ - test/models/bob.rb
64
+ - test/models/remote_bob.rb
65
+ - test/models/user.rb
66
+ - test/models/remote_user.rb