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