diff --git a/lib/active_resource/connection.rb b/lib/active_resource/connection.rb
index c40ed7c070..88bbfe5736 100644
--- a/lib/active_resource/connection.rb
+++ b/lib/active_resource/connection.rb
@@ -5,6 +5,19 @@
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
@@ -12,6 +25,7 @@ module ActiveResource
class Connection
HTTP_FORMAT_HEADER_NAMES = {
get: "Accept",
+ query: "Content-Type",
put: "Content-Type",
post: "Content-Type",
patch: "Content-Type",
@@ -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,
@@ -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 = {})
diff --git a/lib/active_resource/custom_methods.rb b/lib/active_resource/custom_methods.rb
index e61b014994..a35edacf3e 100644
--- a/lib/active_resource/custom_methods.rb
+++ b/lib/active_resource/custom_methods.rb
@@ -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 find class method with the
+ # :from 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
@@ -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?
diff --git a/lib/active_resource/http_mock.rb b/lib/active_resource/http_mock.rb
index bb832808d8..f09332d91b 100644
--- a/lib/active_resource/http_mock.rb
+++ b/lib/active_resource/http_mock.rb
@@ -17,8 +17,8 @@ class InvalidRequestError < StandardError; end # :nodoc:
#
# mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {})
#
- # * http_method - The HTTP method to listen for. This can be +get+, +post+, +patch+, +put+, +delete+ or
- # +head+.
+ # * http_method - The HTTP method to listen for. This can be +get+, +post+, +patch+, +put+, +delete+,
+ # +head+, or +query+.
# * path - A string, starting with a "/", defining the URI that is expected to be
# called.
# * request_headers - Headers that are expected along with the request. This argument uses a
@@ -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
@@ -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 = {})
diff --git a/test/cases/base/custom_methods_test.rb b/test/cases/base/custom_methods_test.rb
index 1a03941c5c..954de0204b 100644
--- a/test/cases/base/custom_methods_test.rb
+++ b/test/cases/base/custom_methods_test.rb
@@ -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
@@ -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")
diff --git a/test/cases/connection_test.rb b/test/cases/connection_test.rb
index 99417f0dba..06ef6dee40 100644
--- a/test/cases/connection_test.rb
+++ b/test/cases/connection_test.rb
@@ -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
@@ -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"]
diff --git a/test/cases/http_mock_test.rb b/test/cases/http_mock_test.rb
index 3e9de19a45..921b616b7f 100644
--- a/test/cases/http_mock_test.rb
+++ b/test/cases/http_mock_test.rb
@@ -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")
@@ -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)