harpy 0.1.0

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.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ -I lib
2
+ --color
3
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in harpy.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ harpy is Copyright © 2011 TalentBox SA
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,168 @@
1
+ Harpy
2
+ ======
3
+
4
+ Client for REST API with HATEOAS
5
+
6
+ Dependencies
7
+ ------------
8
+
9
+ * Ruby 1.8.7 or 1.9.2
10
+ * gem "typhoeus", "~> 0.2.4"
11
+ * gem "activesupport", ">= 3.0.0"
12
+ * gem "activemodel", ">= 3.0.0"
13
+ * gem "hash-deep-merge", "~> 0.1.1"
14
+ * gem "yajl-ruby", "~> 0.8.2"
15
+
16
+ Usage
17
+ -----
18
+
19
+ * Set entry_point url:
20
+
21
+ Harpy.entry_point_url = "http://localhost"
22
+
23
+ * Include `Harpy::Resource` in your model:
24
+
25
+ class MyModel
26
+ include Harpy::Resource
27
+ end
28
+
29
+ # Mass assignment
30
+ model = MyModel.new "firstname" => "Anthony", "lastname" => "Stark"
31
+ model.attributes = {"company" => "Stark Enterprises"}
32
+ model.firstname # => "Anthony"
33
+ model.company # => "Stark Enterprises"
34
+
35
+ # Because model is not persisted you can read any attribute, allowing
36
+ # to use form_for on new resources to which the client doesn't know the
37
+ # existing attributes yet
38
+ model.email # => nil
39
+
40
+ # Fetch by url
41
+ MyModel.from_url "http://localhost/mymodel/1"
42
+ # => instance of MyModel with attributes filled in on 200
43
+ # => nil on 404
44
+ # => raises Harpy::ClientTimeout on timeout
45
+ # => raises Harpy::ClientError on Curl error
46
+ # => raises Harpy::InvalidResponseCode on other response codes
47
+
48
+ # Fetch multiple by url in parallel
49
+ MyModel.from_url ["http://localhost/mymodel/1", "http://localhost/mymodel/2"]
50
+
51
+ # Get index
52
+ MyModel.search
53
+ # will call GET http://localhost/mymodel given the following entry_point response:
54
+ {
55
+ "link": [
56
+ {"rel": "my_model", "href": "http://localhost/mymodel"}
57
+ ]
58
+ }
59
+ # => return an array of MyModel instances on 200
60
+ # => raises Harpy::ClientTimeout on timeout
61
+ # => raises Harpy::ClientError on Curl error
62
+ # => raises Harpy::InvalidResponseCode on other response codes
63
+
64
+ # Search by first_name
65
+ MyModel.search :firstname => "Anthony" # GET http://localhost/mymodel?firstname=Anthony
66
+
67
+ # Create (POST)
68
+ model = MyModel.new "firstname" => "Anthony"
69
+ model.save # POST http://localhost/mymodel with {"firstname":"Anthony"}
70
+
71
+ # Get an existing resource by url:
72
+ model = MyModel.from_url "http://localhost/mymodel/1"
73
+ # if the service returns the following response:
74
+ {
75
+ "firstname": "Anthony",
76
+ "lastname": null,
77
+ "urn": "urn:mycompany:mymodel:1"
78
+ "link" => [
79
+ {"rel" => "self", "href" => "http://localhost/mymodel/1"},
80
+ {"rel" => "accounts", "href" => "http://localhost/mymodel/1/accounts"}
81
+ ]
82
+ }
83
+ # we can then do:
84
+ model.firstname # => "Anthony"
85
+ model.link "self" # => "http://localhost/mymodel/1"
86
+ model.link :accounts # => "http://localhost/mymodel/1/accounts"
87
+
88
+ # Update (PUT) requires resource to have both urn and link to self
89
+ model.attributes = {"firstname" => "Tony"}
90
+ model.save # PUT http://localhost/mymodel/1
91
+
92
+ # The resource is persisted once it has an urn:
93
+ model.persisted? # => true
94
+
95
+ # If persisted you can no longer read undefined attributes:
96
+ model.lastname # => nil
97
+ model.email # => will raise NoMethodError
98
+
99
+ * To find a resource by id you need to define `.urn`:
100
+
101
+ class MyModel
102
+ include Harpy::Resource
103
+ def self.urn(id)
104
+ "urn:mycompany:mymodel:#{id}"
105
+ end
106
+ end
107
+
108
+ model = MyModel.from_id 1 # will GET http://localhost/urn:mycompany:mymodel:1
109
+ # expecting a permanent redirect (301) to follow or not found (404)
110
+
111
+ * Rel name to search for in entry_point when getting index can be overridden:
112
+
113
+ class MyCustomModel
114
+ include Harpy::Resource
115
+ def self.resource_name
116
+ "custom_model"
117
+ end
118
+ end
119
+
120
+ * or you can use `.with_url(url)` for getting index of nested resources:
121
+
122
+ class Account
123
+ include Harpy::Resource
124
+ def users
125
+ User.with_url(link "user") do
126
+ User.search
127
+ end
128
+ end
129
+ end
130
+ class User
131
+ include Harpy::Resource
132
+ end
133
+
134
+ * you can override `#url_collection` to create nested resources:
135
+
136
+ class Account
137
+ include Harpy::Resource
138
+ end
139
+ class User
140
+ include Harpy::Resource
141
+ attr_accessor :account
142
+ def url_collection
143
+ account ? account.link("user") : super
144
+ end
145
+ end
146
+
147
+ * Fetch multiple resources in parallel:
148
+
149
+ class FirstModel
150
+ include Harpy::Resource
151
+ end
152
+ class SecondModel
153
+ include Harpy::Resource
154
+ end
155
+
156
+ Harpy::Resource.from_url({
157
+ FirstModel => ["http://localhost/firstmodel/1", "http://localhost/firstmodel/2"],
158
+ SecondModel => ["http://localhost/secondmodel/1"],
159
+ })
160
+ # => {FirstModel => [...], SecondModel => [...]}
161
+
162
+ About
163
+ -----
164
+
165
+ License
166
+ -------
167
+
168
+ harpy is Copyright © 2011 TalentBox SA. It is free software, and may be redistributed under the terms specified in the LICENSE file.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new('spec')
7
+
8
+ task :default => :spec
data/harpy.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "harpy/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "harpy"
7
+ s.version = Harpy::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Joseph HALTER", "Jonathan TRON"]
10
+ s.email = ["joseph.halter@thetalentbox.com", "jonathan.tron@thetalentbox.com"]
11
+ s.homepage = "https://github.com/TalentBox/harpy"
12
+ s.summary = %q{Client for REST API}
13
+ s.description = %q{Client for REST API with HATEOAS}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_runtime_dependency("typhoeus", ["~> 0.2.4"])
21
+ s.add_runtime_dependency("activesupport", [">= 3.0.0"])
22
+ s.add_runtime_dependency("activemodel", [">= 3.0.0"])
23
+ s.add_runtime_dependency("hash-deep-merge", ["~> 0.1.1"])
24
+ s.add_runtime_dependency("yajl-ruby", ["~> 0.8.2"])
25
+
26
+ s.add_development_dependency("rake", ["~> 0.8.7"])
27
+ s.add_development_dependency("rspec", ["~> 2.6.0"])
28
+ s.add_development_dependency("rocco", ["~> 0.7"])
29
+ end
@@ -0,0 +1,66 @@
1
+ require "typhoeus"
2
+ require "hash_deep_merge"
3
+
4
+ module Harpy
5
+ class Client
6
+ attr_accessor :options
7
+
8
+ def initialize(opts=nil)
9
+ self.options = (opts || {})
10
+ end
11
+
12
+ def get(url_or_urls, opts=nil)
13
+ request :get, url_or_urls, opts
14
+ end
15
+
16
+ def head(url_or_urls, opts=nil)
17
+ request :head, url_or_urls, opts
18
+ end
19
+
20
+ def post(url_or_urls, opts=nil)
21
+ request :post, url_or_urls, opts
22
+ end
23
+
24
+ def put(url_or_urls, opts=nil)
25
+ request :put, url_or_urls, opts
26
+ end
27
+
28
+ def patch(url_or_urls, opts=nil)
29
+ request :patch, url_or_urls, opts
30
+ end
31
+
32
+ def delete(url_or_urls, opts=nil)
33
+ request :delete, url_or_urls, opts
34
+ end
35
+
36
+ def run(requests)
37
+ requests.each{|request| Typhoeus::Hydra.hydra.queue request}
38
+ Typhoeus::Hydra.hydra.run
39
+ requests.collect(&:response)
40
+ end
41
+
42
+ def invalid_code(response)
43
+ if response.timed_out?
44
+ raise ClientTimeout
45
+ elsif response.code.zero?
46
+ raise ClientError, response.curl_error_message
47
+ else
48
+ raise InvalidResponseCode, response.code.to_s
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def request(method, urls, opts=nil)
55
+ opts = options.deep_merge(opts || {})
56
+ case urls
57
+ when Array
58
+ requests = urls.collect do |url|
59
+ Typhoeus::Request.new url, opts.merge(:method => method)
60
+ end
61
+ else
62
+ Typhoeus::Request.run urls, opts.merge(:method => method)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,35 @@
1
+ require "yajl"
2
+
3
+ module Harpy
4
+ class EntryPoint
5
+ attr_accessor :url
6
+
7
+ def initialize(url)
8
+ self.url = url
9
+ end
10
+
11
+ def resource_url(resource_type)
12
+ response = Harpy.client.get url
13
+ case response.code
14
+ when 200
15
+ body = Yajl::Parser.parse response.body
16
+ link = (body["link"] || []).detect{|link| link["rel"] == resource_type}
17
+ link["href"] if link
18
+ else
19
+ Harpy.client.invalid_code response
20
+ end
21
+ end
22
+
23
+ def urn(urn)
24
+ response = Harpy.client.get "#{url}/#{urn}"
25
+ case response.code
26
+ when 301
27
+ response.headers_hash["Location"]
28
+ when 404
29
+ nil
30
+ else
31
+ Harpy.client.invalid_code response
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,197 @@
1
+ require "harpy/client"
2
+ require "active_support"
3
+ require "active_support/core_ext/object/blank"
4
+ require "active_support/core_ext/numeric/bytes"
5
+ require "active_model"
6
+ require "yajl"
7
+
8
+ module Harpy
9
+ module Resource
10
+ extend ActiveSupport::Concern
11
+
12
+ included do |base|
13
+ base.extend ActiveModel::Naming
14
+ base.send :include, ActiveModel::Conversion
15
+ base.extend ActiveModel::Translation
16
+ base.extend ActiveModel::Callbacks
17
+ base.send :include, ActiveModel::Validations
18
+ base.send :include, ActiveModel::Validations::Callbacks
19
+ base.define_model_callbacks :save, :create, :update, :destroy, :only => [:before, :after]
20
+ end
21
+
22
+ def self.from_url(hash)
23
+ results = {}
24
+ hash.each do |klass, urls|
25
+ results[klass] = Harpy.client.get [*urls]
26
+ end
27
+ Harpy.client.run results.values.flatten
28
+ results.each do |klass, requests|
29
+ requests.collect! do |request|
30
+ klass.send :from_url_handler, request.response
31
+ end
32
+ end
33
+ results
34
+ end
35
+
36
+ module ClassMethods
37
+ def from_url(url)
38
+ case url
39
+ when Array
40
+ Harpy.client.run(client.get url).collect{|response| from_url_handler response}
41
+ else
42
+ from_url_handler Harpy.client.get url
43
+ end
44
+ end
45
+
46
+ def from_id(id)
47
+ url = Harpy.entry_point.urn urn(id)
48
+ from_url url if url
49
+ end
50
+
51
+ def urn(id)
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def resource_name
56
+ name.underscore
57
+ end
58
+
59
+ def search(conditions={})
60
+ response = Harpy.client.get url, :params => conditions
61
+ case response.code
62
+ when 200
63
+ parsed = Yajl::Parser.parse response.body
64
+ parsed[resource_name].collect{|model| new model}
65
+ else
66
+ Harpy.client.invalid_code response
67
+ end
68
+ end
69
+
70
+ def with_url(url)
71
+ raise ArgumentError unless block_given?
72
+ key = "#{resource_name}_url"
73
+ old, Thread.current[key] = Thread.current[key], url
74
+ result = yield
75
+ Thread.current[key] = old
76
+ result
77
+ end
78
+
79
+ private
80
+
81
+ def url
82
+ Thread.current["#{resource_name}_url"] || Harpy.entry_point.resource_url(resource_name)
83
+ end
84
+
85
+ def from_url_handler(response)
86
+ case response.code
87
+ when 200
88
+ new Yajl::Parser.parse response.body
89
+ when 404
90
+ nil
91
+ else
92
+ Harpy.client.invalid_code response
93
+ end
94
+ end
95
+ end
96
+
97
+ module InstanceMethods
98
+ def initialize(attrs = {})
99
+ @attrs = attrs
100
+ end
101
+
102
+ def attributes=(attrs)
103
+ @attrs.merge! attrs
104
+ end
105
+
106
+ def as_json
107
+ hash = @attrs.dup
108
+ hash.delete "link"
109
+ hash.delete "urn"
110
+ hash
111
+ end
112
+
113
+ def save
114
+ if valid?
115
+ _run_save_callbacks do
116
+ json = Yajl::Encoder.encode as_json
117
+ raise BodyToBig, "Size: #{json.bytesize} bytes (max 1MB)" if json.bytesize > 1.megabyte
118
+ persisted? ? update(json) : create(json)
119
+ end
120
+ else
121
+ false
122
+ end
123
+ end
124
+
125
+ def link(rel)
126
+ link = (@attrs["link"]||[]).detect{|l| l["rel"]==rel.to_s}
127
+ link["href"] if link
128
+ end
129
+
130
+ def url
131
+ link "self"
132
+ end
133
+
134
+ def url_collection
135
+ Harpy.entry_point.resource_url self.class.resource_name
136
+ end
137
+
138
+ def id
139
+ @attrs["urn"].split(":").last if @attrs["urn"]
140
+ end
141
+
142
+ def persisted?
143
+ @attrs["urn"].present?
144
+ end
145
+
146
+ def inspect
147
+ "<#{self.class.name} @attrs:#{@attrs.inspect} @errors:#{errors.inspect} persisted:#{persisted?}>"
148
+ end
149
+
150
+ def has_key?(key)
151
+ @attrs.has_key? key.to_s
152
+ end
153
+
154
+ private
155
+
156
+ def create(json)
157
+ _run_create_callbacks do
158
+ process_response Harpy.client.post(url_collection, :body => json), :create
159
+ end
160
+ end
161
+
162
+ def update(json)
163
+ _run_update_callbacks do
164
+ raise Harpy::UrlRequired unless url
165
+ process_response Harpy.client.put(url, :body => json), :update
166
+ end
167
+ end
168
+
169
+ def process_response(response, context)
170
+ case response.code
171
+ when 200, 201, 302
172
+ @attrs.merge! Yajl::Parser.parse(response.body)
173
+ true
174
+ when 204
175
+ context==:create ? Harpy.client.invalid_code(response) : true
176
+ when 401
177
+ raise Harpy::Unauthorized, "Server returned a 401 response code"
178
+ when 422
179
+ Yajl::Parser.parse(response.body)["errors"].each do |attr, attr_errors|
180
+ attr_errors.each{|attr_error| errors[attr] = attr_error }
181
+ end
182
+ false
183
+ else
184
+ Harpy.client.invalid_code response
185
+ end
186
+ end
187
+
188
+ def method_missing(method, *args)
189
+ if persisted? && !@attrs.has_key?(method.to_s)
190
+ super
191
+ else
192
+ @attrs[method.to_s]
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,3 @@
1
+ module Harpy
2
+ VERSION = "0.1.0"
3
+ end
data/lib/harpy.rb ADDED
@@ -0,0 +1,46 @@
1
+ module Harpy
2
+ class Exception < ::Exception; end
3
+ class EntryPointRequired < Exception; end
4
+ class UrlRequired < Exception; end
5
+ class BodyToBig < Exception; end
6
+ class ClientTimeout < Exception; end
7
+ class ClientError < Exception; end
8
+ class Unauthorized < Exception; end
9
+ class InvalidResponseCode < Exception; end
10
+
11
+ autoload :Client, "harpy/client"
12
+ autoload :EntryPoint, "harpy/entry_point"
13
+ autoload :Resource, "harpy/resource"
14
+ autoload :BodyToBig, "harpy/resource"
15
+ autoload :UnknownResponseCode, "harpy/resource"
16
+
17
+ def self.client=(new_client)
18
+ @client = new_client
19
+ end
20
+
21
+ def self.client
22
+ @client ||= Client.new
23
+ end
24
+
25
+ def self.entry_point_url=(url)
26
+ @entry_point = EntryPoint.new url
27
+ end
28
+
29
+ def self.entry_point_url
30
+ @entry_point.url if @entry_point
31
+ end
32
+
33
+ def self.entry_point=(value)
34
+ @entry_point = value
35
+ end
36
+
37
+ def self.entry_point
38
+ @entry_point || raise(EntryPointRequired, 'you can setup one with Harpy.entry_point_url = "http://localhost"')
39
+ end
40
+
41
+ def self.reset
42
+ @client = nil
43
+ @entry_point = nil
44
+ end
45
+
46
+ end
@@ -0,0 +1,96 @@
1
+ require "spec_helper"
2
+
3
+ describe Harpy::Client do
4
+ let(:entry_url) { "http://localhost" }
5
+ let(:users_url) { "http://localhost/users" }
6
+
7
+ context "by default" do
8
+ its(:options) { should be_empty }
9
+ end
10
+
11
+ context "initialized with options" do
12
+ let(:options) { {:username => "harpy", :password => "spec"} }
13
+ subject { Harpy::Client.new(options) }
14
+ its(:options) { should == options }
15
+ end
16
+
17
+ [:get, :head, :post, :put, :patch, :delete].each do |method|
18
+ describe "##{method}(url, opts={})" do
19
+ context "with one url" do
20
+ before do
21
+ @expected = Typhoeus::Response.new :code => 200
22
+ Typhoeus::Hydra.hydra.stub(method, entry_url).and_return(@expected)
23
+ end
24
+ it "sends a #{method.to_s.upcase} to the url" do
25
+ subject.send(method, entry_url).should == @expected
26
+ end
27
+ it "merges options" do
28
+ client = Harpy::Client.new :headers => {"Authorization" => "spec"}
29
+ Typhoeus::Hydra.hydra.stub(method, entry_url).and_return(Typhoeus::Response.new :code => 200)
30
+ response = client.send method, entry_url, :headers => {"X-Files" => "Harpy"}
31
+ response.request.headers.should include({"X-Files" => "Harpy", "Authorization" => "spec"})
32
+ end
33
+ end
34
+ context "with multiple urls" do
35
+ it "does not execute requests" do
36
+ lambda {
37
+ subject.send method, [entry_url, users_url]
38
+ }.should_not raise_error Typhoeus::Hydra::NetConnectNotAllowedError
39
+ end
40
+ it "returns one requests per url" do
41
+ requests = subject.send method, [entry_url, users_url]
42
+ requests.size.should == 2
43
+ requests.collect(&:method).should =~ [method, method]
44
+ requests.collect(&:url).should =~ [entry_url, users_url]
45
+ end
46
+ it "merges options" do
47
+ client = Harpy::Client.new :headers => {"Authorization" => "spec"}
48
+ requests = client.send method, [entry_url, users_url], :headers => {"X-Files" => "Harpy"}
49
+ requests.each do |request|
50
+ request.headers.should include({"X-Files" => "Harpy", "Authorization" => "spec"})
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ describe "#run(requests)" do
57
+ before do
58
+ @entry_response = Typhoeus::Response.new :code => 200, :body => "entry"
59
+ @users_response = Typhoeus::Response.new :code => 200, :body => "users"
60
+
61
+ Typhoeus::Hydra.hydra.stub(:get, entry_url).and_return @entry_response
62
+ Typhoeus::Hydra.hydra.stub(:get, users_url).and_return @users_response
63
+ end
64
+ it "executes requests in parallel" do
65
+ Typhoeus::Hydra.hydra.should_receive(:run).once
66
+ subject.run subject.get([entry_url, users_url])
67
+ end
68
+ it "returns responses" do
69
+ responses = subject.run subject.get([entry_url, users_url])
70
+ responses.should =~ [@entry_response, @users_response]
71
+ end
72
+ it "requests response is filled in" do
73
+ requests = subject.get([entry_url, users_url])
74
+ subject.run requests
75
+ requests[0].response.should == @entry_response
76
+ requests[1].response.should == @users_response
77
+ end
78
+ end
79
+ describe "#invalid_code(response)" do
80
+ it "raises Harpy::ClientTimeout on request timeout" do
81
+ lambda {
82
+ subject.invalid_code mock("Response", :timed_out? => true)
83
+ }.should raise_error Harpy::ClientTimeout
84
+ end
85
+ it "raises Harpy::ClientError on code 0" do
86
+ lambda {
87
+ subject.invalid_code mock("Response", :timed_out? => false, :code => 0, :curl_error_message => "Could not connect to server")
88
+ }.should raise_error Harpy::ClientError, "Could not connect to server"
89
+ end
90
+ it "raises Harpy::InvalidResponseCode with code otherwise" do
91
+ lambda {
92
+ subject.invalid_code mock("Response", :timed_out? => false, :code => 404)
93
+ }.should raise_error Harpy::InvalidResponseCode, "404"
94
+ end
95
+ end
96
+ end