diff --git a/Gemfile b/Gemfile
index 473d9b60fd..4c23778ffc 100644
--- a/Gemfile
+++ b/Gemfile
@@ -9,6 +9,7 @@ gem "activesupport", github: "rails/rails", branch: branch
gem "activemodel", github: "rails/rails", branch: branch
gem "activejob", github: "rails/rails", branch: branch
gem "activerecord", github: "rails/rails", branch: branch
+gem "rack"
gem "sqlite3", branch == "7-0-stable" ? "~> 1.4" : nil
gem "rubocop"
@@ -18,6 +19,7 @@ gem "rubocop-performance"
gem "rubocop-rails"
gem "rubocop-rails-omakase"
+gem "minitest", "< 6"
gem "minitest-bisect"
gemspec
diff --git a/lib/active_resource/base.rb b/lib/active_resource/base.rb
index 10312bb6f5..c0dce6b65e 100644
--- a/lib/active_resource/base.rb
+++ b/lib/active_resource/base.rb
@@ -1151,7 +1151,34 @@ def all(*args)
# This is an alias for all. You can pass in all the same
# arguments to this method as you can to all and find(:all)
+ #
+ # #where accepts conditions in one of several formats. In the examples below, the resulting
+ # URL is given as an illustration.
+ #
+ # === \String
+ #
+ # A string is passed as URL query parameters.
+ #
+ # Person.where("name=Matz")
+ # # https://api.people.com/people.json?name=Matz
+ #
+ # === \Hash
+ #
+ # #where will also accept a hash condition, in which the keys are fields and the values
+ # are values to be searched for.
+ #
+ # Fields can be symbols or strings. Values can be single values, arrays, or ranges.
+ #
+ # Person.where(name: "Matz")
+ # # https://api.people.com/people.json?name=Matz
+ #
+ # Person.where(person: { name: "Matz" })
+ # # https://api.people.com/people.json?person[name]=Matz
+ #
+ # Article.where(tags: ["Ruby", "Rails"])
+ # # https://api.people.com/people.json?tags[]=Ruby&tags[]=Rails
def where(clauses = {})
+ clauses = query_format.decode(clauses) if clauses.is_a?(String)
clauses = sanitize_forbidden_attributes(clauses)
raise ArgumentError, "expected a clauses Hash, got #{clauses.inspect}" unless clauses.is_a? Hash
all(params: clauses)
diff --git a/lib/active_resource/formats/url_encoded_format.rb b/lib/active_resource/formats/url_encoded_format.rb
index dc69209fcb..6e3e19f111 100644
--- a/lib/active_resource/formats/url_encoded_format.rb
+++ b/lib/active_resource/formats/url_encoded_format.rb
@@ -1,16 +1,27 @@
# frozen_string_literal: true
-require "active_support/core_ext/array/wrap"
-
module ActiveResource
module Formats
module UrlEncodedFormat
extend self
+ attr_accessor :query_parser # :nodoc:
+
# URL encode the parameters Hash
def encode(params, options = nil)
params.to_query
end
+
+ # URL decode the query string
+ def decode(query, remove_root = true)
+ query = query.delete_prefix("?")
+
+ if query_parser == :rack
+ Rack::Utils.parse_nested_query(query)
+ else
+ URI.decode_www_form(query).to_h
+ end
+ end
end
end
end
diff --git a/lib/active_resource/railtie.rb b/lib/active_resource/railtie.rb
index 800f1208e2..e5f6b90197 100644
--- a/lib/active_resource/railtie.rb
+++ b/lib/active_resource/railtie.rb
@@ -39,5 +39,9 @@ class Railtie < Rails::Railtie
teardown { ActiveResource::HttpMock.reset! }
end
end
+
+ config.after_initialize do
+ Formats::UrlEncodedFormat.query_parser ||= :rack if defined?(Rack::Utils)
+ end
end
end
diff --git a/lib/active_resource/where_clause.rb b/lib/active_resource/where_clause.rb
index c1072122d2..196cc22216 100644
--- a/lib/active_resource/where_clause.rb
+++ b/lib/active_resource/where_clause.rb
@@ -13,6 +13,7 @@ def initialize(resource_class, options = {})
end
def where(clauses = {})
+ clauses = @resource_class.query_format.decode(clauses) if clauses.is_a?(::String)
all(params: clauses)
end
diff --git a/test/cases/finder_test.rb b/test/cases/finder_test.rb
index 10326a1c65..4bd1d1c018 100644
--- a/test/cases/finder_test.rb
+++ b/test/cases/finder_test.rb
@@ -178,6 +178,33 @@ def test_where_clause_with_permitted_params
assert_kind_of StreetAddress, addresses.first
end
+ def test_where_clause_string
+ query = URI.encode_www_form([ [ "id", "1" ] ])
+ ActiveResource::HttpMock.respond_to { |m| m.get "/people.json?" + query, {}, @people_david }
+ people = Person.where(query)
+ assert_equal 1, people.size
+ assert_kind_of Person, people.first
+ assert_equal "David", people.first.name
+ end
+
+ def test_where_clause_string_chained
+ ActiveResource::HttpMock.respond_to { |m| m.get "/people.json?a=1&b=2&c=3&id=2", {}, @people_david }
+ people = Person.where("id=2").where(a: 1).where("b=2").where(c: 3)
+ assert_equal [ "David" ], people.map(&:name)
+ end
+
+ def test_where_clause_string_with_multiple_params
+ previous_query_parser = ActiveResource::Formats::UrlEncodedFormat.query_parser
+ ActiveResource::Formats::UrlEncodedFormat.query_parser = :rack
+
+ query = URI.encode_www_form([ [ "id[]", "1" ], [ "id[]", "2" ] ])
+ ActiveResource::HttpMock.respond_to { |m| m.get "/people.json?" + query, {}, @people }
+ people = Person.where(query)
+ assert_equal [ "Matz", "David" ], people.map(&:name)
+ ensure
+ ActiveResource::Formats::UrlEncodedFormat.query_parser = previous_query_parser
+ end
+
def test_where_with_clause_in
ActiveResource::HttpMock.respond_to { |m| m.get "/people.json?id%5B%5D=2", {}, @people_david }
people = Person.where(id: [ 2 ])
diff --git a/test/cases/formats/url_encoded_format_test.rb b/test/cases/formats/url_encoded_format_test.rb
index a3342eecc7..bdca06c58b 100644
--- a/test/cases/formats/url_encoded_format_test.rb
+++ b/test/cases/formats/url_encoded_format_test.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "abstract_unit"
+require "rack/utils"
class UrlEncodedFormatTest < ActiveSupport::TestCase
test "#encode transforms a Hash into an application/x-www-form-urlencoded query string" do
@@ -10,4 +11,36 @@ class UrlEncodedFormatTest < ActiveSupport::TestCase
assert_equal "a=1&b=2&c%5B%5D=3&c%5B%5D=4", encoded
end
+
+ test "#encode transforms a nested Hash into an application/x-www-form-urlencoded query string" do
+ params = { "person" => { "name" => "Matz" } }
+
+ encoded = ActiveResource::Formats::UrlEncodedFormat.encode(params)
+
+ assert_equal "person%5Bname%5D=Matz", encoded
+ end
+
+ test "#decode transforms an application/x-www-form-urlencoded query string into a Hash" do
+ decoded = ActiveResource::Formats::UrlEncodedFormat.decode("a=1")
+
+ assert_equal({ "a" => "1" }, decoded)
+ end
+
+ test "#decode ignores a ?-prefix" do
+ decoded = ActiveResource::Formats::UrlEncodedFormat.decode("?a=1")
+
+ assert_equal({ "a" => "1" }, decoded)
+ end
+
+ test "#decode transforms an application/x-www-form-urlencoded query string with multiple params into a Hash" do
+ previous_query_parser = ActiveResource::Formats::UrlEncodedFormat.query_parser
+ ActiveResource::Formats::UrlEncodedFormat.query_parser = :rack
+ query = URI.encode_www_form([ [ "a[]", "1" ], [ "a[]", "2" ] ])
+
+ decoded = ActiveResource::Formats::UrlEncodedFormat.decode(query)
+
+ assert_equal({ "a" => [ "1", "2" ] }, decoded)
+ ensure
+ ActiveResource::Formats::UrlEncodedFormat.query_parser = previous_query_parser
+ end
end