Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/active_resource/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,27 @@
require "date"
require "time"

# Define the Net::HTTP::Query class for runtimes where it is not yet available.
#
# See https://github.com/ruby/net-http/pull/309
# See https://www.rfc-editor.org/rfc/rfc10008.html
#
unless Net::HTTP.const_defined?(:Query)
class Net::HTTP::Query < Net::HTTPRequest
METHOD = "QUERY"
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
end
end

module ActiveResource
# Class to handle connections to remote web services.
# This class is used by ActiveResource::Base to interface with REST
# services.
class Connection
HTTP_FORMAT_HEADER_NAMES = {
get: "Accept",
query: "Content-Type",
put: "Content-Type",
post: "Content-Type",
patch: "Content-Type",
Expand All @@ -20,6 +34,7 @@ class Connection
}
HTTP_METHODS = {
get: Net::HTTP::Get,
query: Net::HTTP::Query,
put: Net::HTTP::Put,
post: Net::HTTP::Post,
patch: Net::HTTP::Patch,
Expand Down Expand Up @@ -91,6 +106,16 @@ def get(path, headers = {})
with_auth { request(:get, path, headers) }
end

# Executes a QUERY request (see the HTTP QUERY method,
# https://www.rfc-editor.org/rfc/rfc10008.html, if unfamiliar).
# Used to get (find) resources by transmitting the query in the request
# +body+ instead of the request URI. Like GET, QUERY is safe and idempotent,
# but it accepts a request body so that larger or more complex queries can be
# expressed without encoding them into the path.
def query(path, body = "", headers = {})
with_auth { request(:query, path, body.to_s, headers) }
end

# Executes a DELETE request (see HTTP protocol documentation if unfamiliar).
# Used to delete resources.
def delete(path, headers = {})
Expand Down
21 changes: 21 additions & 0 deletions lib/active_resource/custom_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ def get(custom_method_name, options = {})
derooted.is_a?(Array) ? derooted.map { |e| Formats.remove_root(e) } : derooted
end

# Invokes a QUERY to a given custom REST method. Like GET, QUERY is safe
# and idempotent, but the query is transmitted in the request +body+
# rather than the request URI. For example:
#
# Person.query(:search, {}, { name: "Ryan" }.to_json) # QUERY /people/search.json
# # => [{:id => 1, :name => 'Ryan'}]
#
# Note: the objects returned from this method are not automatically converted
# into ActiveResource::Base instances - they are ordinary Hashes. If you are expecting
# ActiveResource::Base instances, use the <tt>find</tt> class method with the
# <tt>:from</tt> option.
def query(custom_method_name, options = {}, body = "")
hashified = format.decode(connection.query(custom_method_collection_url(custom_method_name, options), body, headers).body)
derooted = Formats.remove_root(hashified)
derooted.is_a?(Array) ? derooted.map { |e| Formats.remove_root(e) } : derooted
end

def post(custom_method_name, options = {}, body = "")
connection.post(custom_method_collection_url(custom_method_name, options), body, headers)
end
Expand Down Expand Up @@ -95,6 +112,10 @@ def get(method_name, options = {})
self.class.format.decode(connection.get(custom_method_element_url(method_name, options), self.class.headers).body)
end

def query(method_name, options = {}, body = "")
self.class.format.decode(connection.query(custom_method_element_url(method_name, options), body, self.class.headers).body)
end

def post(method_name, options = {}, body = nil)
request_body = body.blank? ? encode : body
if new?
Expand Down
8 changes: 4 additions & 4 deletions lib/active_resource/http_mock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class InvalidRequestError < StandardError; end # :nodoc:
#
# mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
#
# * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +patch+, +put+, +delete+ or
# +head+.
# * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +patch+, +put+, +delete+,
# +head+, or +query+.
# * <tt>path</tt> - A string, starting with a "/", defining the URI that is expected to be
# called.
# * <tt>request_headers</tt> - Headers that are expected along with the request. This argument uses a
Expand Down Expand Up @@ -82,7 +82,7 @@ def initialize(responses)
@responses = responses
end

[ :post, :patch, :put, :get, :delete, :head ].each do |method|
[ :post, :patch, :put, :get, :delete, :head, :query ].each do |method|
# def post(path, request_headers = {}, body = nil, status = 200, response_headers = {}, options: {})
# @responses[Request.new(:post, path, nil, request_headers, options)] = Response.new(body || "", status, response_headers)
# end
Expand Down Expand Up @@ -267,7 +267,7 @@ def net_connection_disabled?
end

# body? methods
{ true => %w[post patch put],
{ true => %w[post patch put query],
false => %w[get delete head] }.each do |has_body, methods|
methods.each do |method|
# def post(path, body, headers, options = {})
Expand Down
12 changes: 12 additions & 0 deletions test/cases/base/custom_methods_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def setup
mock.get "/products/1/inventories/1/shallow.json", {}, @inventory
mock.put "/products/1/inventories/1/promote.json?name=Warehouse", {}, nil, 204
mock.delete "/products/1/inventories/1/deactivate.json", {}, nil, 200
mock.query "/people/search.json", {}, @matz_array
mock.query "/people/1/similar.json", {}, @matz
end

Person.user = nil
Expand Down Expand Up @@ -91,6 +93,16 @@ def test_custom_element_method
StreetAddress.find(1, params: { person_id: 1 }).put(:normalize_phone, locale: "US")
end

def test_custom_collection_query_method
# QUERY against a collection URL, transmitting the query in the request body
assert_equal([ { "id" => 1, "name" => "Matz" } ], Person.query(:search, {}, { name: "Matz" }.to_json))
end

def test_custom_element_query_method
# QUERY against an element URL
assert_equal({ "id" => 1, "name" => "Matz" }, Person.find(1).query(:similar, {}, { name: "Matz" }.to_json))
end

def test_custom_new_element_method
# Test POST against a new element URL
ryan = Person.new(name: "Ryan")
Expand Down
13 changes: 13 additions & 0 deletions test/cases/connection_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def setup
mock.post "/people.json", {}, nil, 201, "Location" => "/people/5.json"
mock.post "/members.json", {}, @header, 201, "Location" => "/people/6.json"
mock.head "/people/1.json", {}, nil, 200
mock.query "/people/search.json", {}, @people
mock.query "/members/search.json", @header, @david
end
end

Expand Down Expand Up @@ -188,6 +190,17 @@ def test_head
assert_equal 200, response.code
end

def test_query
people = decode(@conn.query("/people/search.json", { name: "Matz" }.to_json))
assert_equal "Matz", people.dig(0, "person", "name")
assert_equal "David", people.dig(1, "person", "name")
end

def test_query_with_header
david = decode(@conn.query("/members/search.json", { name: "David" }.to_json, @header))
assert_equal "David", david["name"]
end

def test_get_with_header
david = decode(@conn.get("/people/2.json", @header))
assert_equal "David", david["name"]
Expand Down
4 changes: 2 additions & 2 deletions test/cases/http_mock_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class HttpMockTest < ActiveSupport::TestCase
end
end

[ :post, :patch, :put, :get, :delete, :head ].each do |method|
[ :post, :patch, :put, :get, :delete, :head, :query ].each do |method|
test "responds to simple #{method} request" do
ActiveResource::HttpMock.respond_to do |mock|
mock.send(method, "/people/1", { FORMAT_HEADER[method] => "application/json" }, "Response")
Expand Down Expand Up @@ -281,7 +281,7 @@ class HttpMockTest < ActiveSupport::TestCase
end

def request(method, path, headers = {}, body = nil)
if method.in?([ :patch, :put, :post ])
if method.in?([ :patch, :put, :post, :query ])
@http.send(method, path, body, headers)
else
@http.send(method, path, headers)
Expand Down
Loading