activeresource 2.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activeresource might be problematic. Click here for more details.

@@ -0,0 +1,288 @@
1
+ module ActiveResource
2
+ class ResourceInvalid < ClientError #:nodoc:
3
+ end
4
+
5
+ # Active Resource validation is reported to and from this object, which is used by Base#save
6
+ # to determine whether the object in a valid state to be saved. See usage example in Validations.
7
+ class Errors
8
+ include Enumerable
9
+ attr_reader :errors
10
+
11
+ delegate :empty?, :to => :errors
12
+
13
+ def initialize(base) # :nodoc:
14
+ @base, @errors = base, {}
15
+ end
16
+
17
+ # Add an error to the base Active Resource object rather than an attribute.
18
+ #
19
+ # ==== Examples
20
+ # my_folder = Folder.find(1)
21
+ # my_folder.errors.add_to_base("You can't edit an existing folder")
22
+ # my_folder.errors.on_base
23
+ # # => "You can't edit an existing folder"
24
+ #
25
+ # my_folder.errors.add_to_base("This folder has been tagged as frozen")
26
+ # my_folder.valid?
27
+ # # => false
28
+ # my_folder.errors.on_base
29
+ # # => ["You can't edit an existing folder", "This folder has been tagged as frozen"]
30
+ #
31
+ def add_to_base(msg)
32
+ add(:base, msg)
33
+ end
34
+
35
+ # Adds an error to an Active Resource object's attribute (named for the +attribute+ parameter)
36
+ # with the error message in +msg+.
37
+ #
38
+ # ==== Examples
39
+ # my_resource = Node.find(1)
40
+ # my_resource.errors.add('name', 'can not be "base"') if my_resource.name == 'base'
41
+ # my_resource.errors.on('name')
42
+ # # => 'can not be "base"!'
43
+ #
44
+ # my_resource.errors.add('desc', 'can not be blank') if my_resource.desc == ''
45
+ # my_resource.valid?
46
+ # # => false
47
+ # my_resource.errors.on('desc')
48
+ # # => 'can not be blank!'
49
+ #
50
+ def add(attribute, msg)
51
+ @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
52
+ @errors[attribute.to_s] << msg
53
+ end
54
+
55
+ # Returns true if the specified +attribute+ has errors associated with it.
56
+ #
57
+ # ==== Examples
58
+ # my_resource = Disk.find(1)
59
+ # my_resource.errors.add('location', 'must be Main') unless my_resource.location == 'Main'
60
+ # my_resource.errors.on('location')
61
+ # # => 'must be Main!'
62
+ #
63
+ # my_resource.errors.invalid?('location')
64
+ # # => true
65
+ # my_resource.errors.invalid?('name')
66
+ # # => false
67
+ def invalid?(attribute)
68
+ !@errors[attribute.to_s].nil?
69
+ end
70
+
71
+ # A method to return the errors associated with +attribute+, which returns nil, if no errors are
72
+ # associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
73
+ # or an array of error messages if more than one error is associated with the specified +attribute+.
74
+ #
75
+ # ==== Examples
76
+ # my_person = Person.new(params[:person])
77
+ # my_person.errors.on('login')
78
+ # # => nil
79
+ #
80
+ # my_person.errors.add('login', 'can not be empty') if my_person.login == ''
81
+ # my_person.errors.on('login')
82
+ # # => 'can not be empty'
83
+ #
84
+ # my_person.errors.add('login', 'can not be longer than 10 characters') if my_person.login.length > 10
85
+ # my_person.errors.on('login')
86
+ # # => ['can not be empty', 'can not be longer than 10 characters']
87
+ def on(attribute)
88
+ errors = @errors[attribute.to_s]
89
+ return nil if errors.nil?
90
+ errors.size == 1 ? errors.first : errors
91
+ end
92
+
93
+ alias :[] :on
94
+
95
+ # A method to return errors assigned to +base+ object through add_to_base, which returns nil, if no errors are
96
+ # associated with the specified +attribute+, the error message if one error is associated with the specified +attribute+,
97
+ # or an array of error messages if more than one error is associated with the specified +attribute+.
98
+ #
99
+ # ==== Examples
100
+ # my_account = Account.find(1)
101
+ # my_account.errors.on_base
102
+ # # => nil
103
+ #
104
+ # my_account.errors.add_to_base("This account is frozen")
105
+ # my_account.errors.on_base
106
+ # # => "This account is frozen"
107
+ #
108
+ # my_account.errors.add_to_base("This account has been closed")
109
+ # my_account.errors.on_base
110
+ # # => ["This account is frozen", "This account has been closed"]
111
+ #
112
+ def on_base
113
+ on(:base)
114
+ end
115
+
116
+ # Yields each attribute and associated message per error added.
117
+ #
118
+ # ==== Examples
119
+ # my_person = Person.new(params[:person])
120
+ #
121
+ # my_person.errors.add('login', 'can not be empty') if my_person.login == ''
122
+ # my_person.errors.add('password', 'can not be empty') if my_person.password == ''
123
+ # messages = ''
124
+ # my_person.errors.each {|attr, msg| messages += attr.humanize + " " + msg + "<br />"}
125
+ # messages
126
+ # # => "Login can not be empty<br />Password can not be empty<br />"
127
+ #
128
+ def each
129
+ @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
130
+ end
131
+
132
+ # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
133
+ # through iteration as "First name can't be empty".
134
+ #
135
+ # ==== Examples
136
+ # my_person = Person.new(params[:person])
137
+ #
138
+ # my_person.errors.add('login', 'can not be empty') if my_person.login == ''
139
+ # my_person.errors.add('password', 'can not be empty') if my_person.password == ''
140
+ # messages = ''
141
+ # my_person.errors.each_full {|msg| messages += msg + "<br/>"}
142
+ # messages
143
+ # # => "Login can not be empty<br />Password can not be empty<br />"
144
+ #
145
+ def each_full
146
+ full_messages.each { |msg| yield msg }
147
+ end
148
+
149
+ # Returns all the full error messages in an array.
150
+ #
151
+ # ==== Examples
152
+ # my_person = Person.new(params[:person])
153
+ #
154
+ # my_person.errors.add('login', 'can not be empty') if my_person.login == ''
155
+ # my_person.errors.add('password', 'can not be empty') if my_person.password == ''
156
+ # messages = ''
157
+ # my_person.errors.full_messages.each {|msg| messages += msg + "<br/>"}
158
+ # messages
159
+ # # => "Login can not be empty<br />Password can not be empty<br />"
160
+ #
161
+ def full_messages
162
+ full_messages = []
163
+
164
+ @errors.each_key do |attr|
165
+ @errors[attr].each do |msg|
166
+ next if msg.nil?
167
+
168
+ if attr == "base"
169
+ full_messages << msg
170
+ else
171
+ full_messages << [attr.humanize, msg].join(' ')
172
+ end
173
+ end
174
+ end
175
+ full_messages
176
+ end
177
+
178
+ def clear
179
+ @errors = {}
180
+ end
181
+
182
+ # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
183
+ # with this as well.
184
+ #
185
+ # ==== Examples
186
+ # my_person = Person.new(params[:person])
187
+ # my_person.errors.size
188
+ # # => 0
189
+ #
190
+ # my_person.errors.add('login', 'can not be empty') if my_person.login == ''
191
+ # my_person.errors.add('password', 'can not be empty') if my_person.password == ''
192
+ # my_person.error.size
193
+ # # => 2
194
+ #
195
+ def size
196
+ @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
197
+ end
198
+
199
+ alias_method :count, :size
200
+ alias_method :length, :size
201
+
202
+ # Grabs errors from the XML response.
203
+ def from_xml(xml)
204
+ clear
205
+ humanized_attributes = @base.attributes.keys.inject({}) { |h, attr_name| h.update(attr_name.humanize => attr_name) }
206
+ messages = Hash.from_xml(xml)['errors']['error'] rescue []
207
+ messages.each do |message|
208
+ attr_message = humanized_attributes.keys.detect do |attr_name|
209
+ if message[0, attr_name.size + 1] == "#{attr_name} "
210
+ add humanized_attributes[attr_name], message[(attr_name.size + 1)..-1]
211
+ end
212
+ end
213
+
214
+ add_to_base message if attr_message.nil?
215
+ end
216
+ end
217
+ end
218
+
219
+ # Module to allow validation of ActiveResource objects, which creates an Errors instance for every resource.
220
+ # Methods are implemented by overriding +Base#validate+ or its variants Each of these methods can inspect
221
+ # the state of the object, which usually means ensuring that a number of attributes have a certain value
222
+ # (such as not empty, within a given range, matching a certain regular expression and so on).
223
+ #
224
+ # ==== Example
225
+ #
226
+ # class Person < ActiveResource::Base
227
+ # self.site = "http://www.localhost.com:3000/"
228
+ # protected
229
+ # def validate
230
+ # errors.add_on_empty %w( first_name last_name )
231
+ # errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
232
+ # end
233
+ #
234
+ # def validate_on_create # is only run the first time a new object is saved
235
+ # unless valid_member?(self)
236
+ # errors.add("membership_discount", "has expired")
237
+ # end
238
+ # end
239
+ #
240
+ # def validate_on_update
241
+ # errors.add_to_base("No changes have occurred") if unchanged_attributes?
242
+ # end
243
+ # end
244
+ #
245
+ # person = Person.new("first_name" => "Jim", "phone_number" => "I will not tell you.")
246
+ # person.save # => false (and doesn't do the save)
247
+ # person.errors.empty? # => false
248
+ # person.errors.count # => 2
249
+ # person.errors.on "last_name" # => "can't be empty"
250
+ # person.attributes = { "last_name" => "Halpert", "phone_number" => "555-5555" }
251
+ # person.save # => true (and person is now saved to the remote service)
252
+ #
253
+ module Validations
254
+ def self.included(base) # :nodoc:
255
+ base.class_eval do
256
+ alias_method_chain :save, :validation
257
+ end
258
+ end
259
+
260
+ # Validate a resource and save (POST) it to the remote web service.
261
+ def save_with_validation
262
+ save_without_validation
263
+ true
264
+ rescue ResourceInvalid => error
265
+ errors.from_xml(error.response.body)
266
+ false
267
+ end
268
+
269
+ # Checks for errors on an object (i.e., is resource.errors empty?).
270
+ #
271
+ # ==== Examples
272
+ # my_person = Person.create(params[:person])
273
+ # my_person.valid?
274
+ # # => true
275
+ #
276
+ # my_person.errors.add('login', 'can not be empty') if my_person.login == ''
277
+ # my_person.valid?
278
+ # # => false
279
+ def valid?
280
+ errors.empty?
281
+ end
282
+
283
+ # Returns the Errors object that holds all information about attribute error messages.
284
+ def errors
285
+ @errors ||= Errors.new(self)
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveResource
2
+ module VERSION #:nodoc:
3
+ MAJOR = 2
4
+ MINOR = 0
5
+ TINY = 1
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1 @@
1
+ require 'active_resource'
@@ -0,0 +1,10 @@
1
+ require 'test/unit'
2
+
3
+ $:.unshift "#{File.dirname(__FILE__)}/../lib"
4
+ require 'active_resource'
5
+ require 'active_resource/http_mock'
6
+
7
+ $:.unshift "#{File.dirname(__FILE__)}/../test"
8
+ require 'setter_trap'
9
+
10
+ ActiveResource::Base.logger = Logger.new("#{File.dirname(__FILE__)}/debug.log")
@@ -0,0 +1,82 @@
1
+ require "#{File.dirname(__FILE__)}/abstract_unit"
2
+ require 'base64'
3
+
4
+ class AuthorizationTest < Test::Unit::TestCase
5
+ Response = Struct.new(:code)
6
+
7
+ def setup
8
+ @conn = ActiveResource::Connection.new('http://localhost')
9
+ @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
10
+ @david = { :id => 2, :name => 'David' }.to_xml(:root => 'person')
11
+ @authenticated_conn = ActiveResource::Connection.new("http://david:test123@localhost")
12
+ @authorization_request_header = { 'Authorization' => 'Basic ZGF2aWQ6dGVzdDEyMw==' }
13
+
14
+ ActiveResource::HttpMock.respond_to do |mock|
15
+ mock.get "/people/2.xml", @authorization_request_header, @david
16
+ mock.put "/people/2.xml", @authorization_request_header, nil, 204
17
+ mock.delete "/people/2.xml", @authorization_request_header, nil, 200
18
+ mock.post "/people/2/addresses.xml", @authorization_request_header, nil, 201, 'Location' => '/people/1/addresses/5'
19
+ end
20
+ end
21
+
22
+ def test_authorization_header
23
+ authorization_header = @authenticated_conn.send!(:authorization_header)
24
+ assert_equal @authorization_request_header['Authorization'], authorization_header['Authorization']
25
+ authorization = authorization_header["Authorization"].to_s.split
26
+
27
+ assert_equal "Basic", authorization[0]
28
+ assert_equal ["david", "test123"], Base64.decode64(authorization[1]).split(":")[0..1]
29
+ end
30
+
31
+ def test_authorization_header_with_username_but_no_password
32
+ @conn = ActiveResource::Connection.new("http://david:@localhost")
33
+ authorization_header = @conn.send!(:authorization_header)
34
+ authorization = authorization_header["Authorization"].to_s.split
35
+
36
+ assert_equal "Basic", authorization[0]
37
+ assert_equal ["david"], Base64.decode64(authorization[1]).split(":")[0..1]
38
+ end
39
+
40
+ def test_authorization_header_with_password_but_no_username
41
+ @conn = ActiveResource::Connection.new("http://:test123@localhost")
42
+ authorization_header = @conn.send!(:authorization_header)
43
+ authorization = authorization_header["Authorization"].to_s.split
44
+
45
+ assert_equal "Basic", authorization[0]
46
+ assert_equal ["", "test123"], Base64.decode64(authorization[1]).split(":")[0..1]
47
+ end
48
+
49
+ def test_get
50
+ david = @authenticated_conn.get("/people/2.xml")
51
+ assert_equal "David", david["name"]
52
+ end
53
+
54
+ def test_post
55
+ response = @authenticated_conn.post("/people/2/addresses.xml")
56
+ assert_equal "/people/1/addresses/5", response["Location"]
57
+ end
58
+
59
+ def test_put
60
+ response = @authenticated_conn.put("/people/2.xml")
61
+ assert_equal 204, response.code
62
+ end
63
+
64
+ def test_delete
65
+ response = @authenticated_conn.delete("/people/2.xml")
66
+ assert_equal 200, response.code
67
+ end
68
+
69
+ def test_raises_invalid_request_on_unauthorized_requests
70
+ assert_raises(ActiveResource::InvalidRequestError) { @conn.post("/people/2.xml") }
71
+ assert_raises(ActiveResource::InvalidRequestError) { @conn.post("/people/2/addresses.xml") }
72
+ assert_raises(ActiveResource::InvalidRequestError) { @conn.put("/people/2.xml") }
73
+ assert_raises(ActiveResource::InvalidRequestError) { @conn.delete("/people/2.xml") }
74
+ end
75
+
76
+ protected
77
+ def assert_response_raises(klass, code)
78
+ assert_raise(klass, "Expected response code #{code} to raise #{klass}") do
79
+ @conn.send!(:handle_response, Response.new(code))
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,96 @@
1
+ require "#{File.dirname(__FILE__)}/../abstract_unit"
2
+ require "#{File.dirname(__FILE__)}/../fixtures/person"
3
+ require "#{File.dirname(__FILE__)}/../fixtures/street_address"
4
+
5
+ class CustomMethodsTest < Test::Unit::TestCase
6
+ def setup
7
+ @matz = { :id => 1, :name => 'Matz' }.to_xml(:root => 'person')
8
+ @matz_deep = { :id => 1, :name => 'Matz', :other => 'other' }.to_xml(:root => 'person')
9
+ @matz_array = [{ :id => 1, :name => 'Matz' }].to_xml(:root => 'people')
10
+ @ryan = { :name => 'Ryan' }.to_xml(:root => 'person')
11
+ @addy = { :id => 1, :street => '12345 Street' }.to_xml(:root => 'address')
12
+ @addy_deep = { :id => 1, :street => '12345 Street', :zip => "27519" }.to_xml(:root => 'address')
13
+ @default_request_headers = { 'Content-Type' => 'application/xml' }
14
+
15
+ ActiveResource::HttpMock.respond_to do |mock|
16
+ mock.get "/people/1.xml", {}, @matz
17
+ mock.get "/people/1/shallow.xml", {}, @matz
18
+ mock.get "/people/1/deep.xml", {}, @matz_deep
19
+ mock.get "/people/retrieve.xml?name=Matz", {}, @matz_array
20
+ mock.get "/people/managers.xml", {}, @matz_array
21
+ mock.post "/people/hire.xml?name=Matz", {}, nil, 201
22
+ mock.put "/people/1/promote.xml?position=Manager", {}, nil, 204
23
+ mock.put "/people/promote.xml?name=Matz", {}, nil, 204, {}
24
+ mock.put "/people/sort.xml?by=name", {}, nil, 204
25
+ mock.delete "/people/deactivate.xml?name=Matz", {}, nil, 200
26
+ mock.delete "/people/1/deactivate.xml", {}, nil, 200
27
+ mock.post "/people/new/register.xml", {}, @ryan, 201, 'Location' => '/people/5.xml'
28
+ mock.post "/people/1/register.xml", {}, @matz, 201
29
+ mock.get "/people/1/addresses/1.xml", {}, @addy
30
+ mock.get "/people/1/addresses/1/deep.xml", {}, @addy_deep
31
+ mock.put "/people/1/addresses/1/normalize_phone.xml?locale=US", {}, nil, 204
32
+ mock.put "/people/1/addresses/sort.xml?by=name", {}, nil, 204
33
+ mock.post "/people/1/addresses/new/link.xml", {}, { :street => '12345 Street' }.to_xml(:root => 'address'), 201, 'Location' => '/people/1/addresses/2.xml'
34
+ end
35
+ end
36
+
37
+ def teardown
38
+ ActiveResource::HttpMock.reset!
39
+ end
40
+
41
+ def test_custom_collection_method
42
+ # GET
43
+ assert_equal([{ "id" => 1, "name" => 'Matz' }], Person.get(:retrieve, :name => 'Matz'))
44
+
45
+ # POST
46
+ assert_equal(ActiveResource::Response.new("", 201, {}), Person.post(:hire, :name => 'Matz'))
47
+
48
+ # PUT
49
+ assert_equal ActiveResource::Response.new("", 204, {}),
50
+ Person.put(:promote, {:name => 'Matz'}, 'atestbody')
51
+ assert_equal ActiveResource::Response.new("", 204, {}), Person.put(:sort, :by => 'name')
52
+
53
+ # DELETE
54
+ Person.delete :deactivate, :name => 'Matz'
55
+
56
+ # Nested resource
57
+ assert_equal ActiveResource::Response.new("", 204, {}), StreetAddress.put(:sort, :person_id => 1, :by => 'name')
58
+ end
59
+
60
+ def test_custom_element_method
61
+ # Test GET against an element URL
62
+ assert_equal Person.find(1).get(:shallow), {"id" => 1, "name" => 'Matz'}
63
+ assert_equal Person.find(1).get(:deep), {"id" => 1, "name" => 'Matz', "other" => 'other'}
64
+
65
+ # Test PUT against an element URL
66
+ assert_equal ActiveResource::Response.new("", 204, {}), Person.find(1).put(:promote, {:position => 'Manager'}, 'body')
67
+
68
+ # Test DELETE against an element URL
69
+ assert_equal ActiveResource::Response.new("", 200, {}), Person.find(1).delete(:deactivate)
70
+
71
+ # With nested resources
72
+ assert_equal StreetAddress.find(1, :params => { :person_id => 1 }).get(:deep),
73
+ { "id" => 1, "street" => '12345 Street', "zip" => "27519" }
74
+ assert_equal ActiveResource::Response.new("", 204, {}),
75
+ StreetAddress.find(1, :params => { :person_id => 1 }).put(:normalize_phone, :locale => 'US')
76
+ end
77
+
78
+ def test_custom_new_element_method
79
+ # Test POST against a new element URL
80
+ ryan = Person.new(:name => 'Ryan')
81
+ assert_equal ActiveResource::Response.new(@ryan, 201, {'Location' => '/people/5.xml'}), ryan.post(:register)
82
+
83
+ # Test POST against a nested collection URL
84
+ addy = StreetAddress.new(:street => '123 Test Dr.', :person_id => 1)
85
+ assert_equal ActiveResource::Response.new({ :street => '12345 Street' }.to_xml(:root => 'address'),
86
+ 201, {'Location' => '/people/1/addresses/2.xml'}),
87
+ addy.post(:link)
88
+
89
+ matz = Person.new(:id => 1, :name => 'Matz')
90
+ assert_equal ActiveResource::Response.new(@matz, 201), matz.post(:register)
91
+ end
92
+
93
+ def test_find_custom_resources
94
+ assert_equal 'Matz', Person.find(:all, :from => :managers).first.name
95
+ end
96
+ end