Skip to content

Commit 62b941e

Browse files
authored
Merge pull request #435 from seanpdoyle/query-format
Introduce `Base.query_format` for URL encoding values
2 parents 2b4d48a + a5072e7 commit 62b941e

File tree

7 files changed

+125
-18
lines changed

7 files changed

+125
-18
lines changed

README.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ response:
132132
```ruby
133133
# Expects a response of
134134
#
135-
# {"id":1,"first":"Tyler","last":"Durden"}
135+
# {"id":1,"first_name":"Tyler","last_name":"Durden"}
136136
#
137137
# for GET http://api.people.com:3000/people/1.json
138138
#
@@ -144,14 +144,14 @@ JSON element becoming an attribute on the object.
144144

145145
```ruby
146146
tyler.is_a? Person # => true
147-
tyler.last # => 'Durden'
147+
tyler.last_name # => 'Durden'
148148
```
149149

150150
Any complex element (one that contains other elements) becomes its own object:
151151

152152
```ruby
153153
# With this response:
154-
# {"id":1,"first":"Tyler","address":{"street":"Paper St.","state":"CA"}}
154+
# {"id":1,"first_name":"Tyler","address":{"street":"Paper St.","state":"CA"}}
155155
#
156156
# for GET http://api.people.com:3000/people/1.json
157157
#
@@ -166,15 +166,30 @@ Collections can also be requested in a similar fashion
166166
# Expects a response of
167167
#
168168
# [
169-
# {"id":1,"first":"Tyler","last":"Durden"},
170-
# {"id":2,"first":"Tony","last":"Stark",}
169+
# {"id":1,"first_name":"Tyler","last_name":"Durden"},
170+
# {"id":2,"first_name":"Tony","last_name":"Stark",}
171171
# ]
172172
#
173173
# for GET http://api.people.com:3000/people.json
174174
#
175175
people = Person.all
176-
people.first # => <Person::xxx 'first' => 'Tyler' ...>
177-
people.last # => <Person::xxx 'first' => 'Tony' ...>
176+
people.first # => <Person::xxx 'first_name' => 'Tyler' ...>
177+
people.last # => <Person::xxx 'first_name' => 'Tony' ...>
178+
```
179+
180+
Collections can be filtered with query parameters
181+
182+
```ruby
183+
# Expects a response of
184+
#
185+
# [
186+
# {"id":1,"first_name":"Tyler","last_name":"Durden"},
187+
# ]
188+
#
189+
# for GET http://api.people.com:3000/people.json?last_name=Durden
190+
#
191+
people = Person.where(last_name: "Durden")
192+
people.first # => <Person::xxx 'first_name' => 'Tyler' ...>
178193
```
179194

180195
### Create
@@ -185,12 +200,12 @@ id of the newly created resource is parsed out of the Location response header a
185200
as the id of the ARes object.
186201

187202
```ruby
188-
# {"first":"Tyler","last":"Durden"}
203+
# {"first_name":"Tyler","last_name":"Durden"}
189204
#
190205
# is submitted as the body on
191206
#
192-
# if include_root_in_json is not set or set to false => {"first":"Tyler"}
193-
# if include_root_in_json is set to true => {"person":{"first":"Tyler"}}
207+
# if include_root_in_json is not set or set to false => {"first_name":"Tyler"}
208+
# if include_root_in_json is set to true => {"person":{"first_name":"Tyler"}}
194209
#
195210
# POST http://api.people.com:3000/people.json
196211
#
@@ -199,7 +214,7 @@ as the id of the ARes object.
199214
#
200215
# Response (201): Location: http://api.people.com:3000/people/2
201216
#
202-
tyler = Person.new(:first => 'Tyler')
217+
tyler = Person.new(:first_name => 'Tyler')
203218
tyler.new? # => true
204219
tyler.save # => true
205220
tyler.new? # => false
@@ -213,21 +228,21 @@ with the exception that no response headers are needed -- just an empty response
213228
server side was successful.
214229

215230
```ruby
216-
# {"first":"Tyler"}
231+
# {"first_name":"Tyler"}
217232
#
218233
# is submitted as the body on
219234
#
220-
# if include_root_in_json is not set or set to false => {"first":"Tyler"}
221-
# if include_root_in_json is set to true => {"person":{"first":"Tyler"}}
235+
# if include_root_in_json is not set or set to false => {"first_name":"Tyler"}
236+
# if include_root_in_json is set to true => {"person":{"first_name":"Tyler"}}
222237
#
223238
# PUT http://api.people.com:3000/people/1.json
224239
#
225240
# when save is called on an existing Person object. An empty response is
226241
# is expected with code (204)
227242
#
228243
tyler = Person.find(1)
229-
tyler.first # => 'Tyler'
230-
tyler.first = 'Tyson'
244+
tyler.first_name # => 'Tyler'
245+
tyler.first_name = 'Tyson'
231246
tyler.save # => true
232247
```
233248

lib/active_resource/base.rb

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ def self.logger=(logger)
371371
@@logger = logger
372372
end
373373

374+
class_attribute :_query_format
374375
class_attribute :_format
375376
class_attribute :_collection_parser
376377
class_attribute :include_format_in_path
@@ -614,6 +615,24 @@ def auth_type=(auth_type)
614615
@auth_type = auth_type
615616
end
616617

618+
# Sets the URL format that attributes are sent and received in from a mime type reference:
619+
#
620+
# Person.query_format = ActiveResource::Formats::UrlEncodedFormat
621+
# Person.where(first_name: "Matz") # => GET /people.json?first_name=Matz
622+
#
623+
# Person.query_format = CustomCamelcaseUrlEncodedFormat
624+
# Person.where(first_name: "Matz") # => GET /people.json?firstName=Matz
625+
#
626+
# Default format is <tt>ActiveResource::Formats::UrlEncodedFormat</tt>.
627+
def query_format=(mime_type_reference_or_format)
628+
self._query_format = mime_type_reference_or_format
629+
end
630+
631+
# Returns the current parameters format, default is ActiveResource::Formats::UrlEncodedFormat
632+
def query_format
633+
self._query_format || ActiveResource::Formats::UrlEncodedFormat
634+
end
635+
617636
# Sets the format that attributes are sent and received in from a mime type reference:
618637
#
619638
# Person.format = :json
@@ -1023,7 +1042,8 @@ def create!(attributes = {})
10231042
# ==== Options
10241043
#
10251044
# * <tt>:from</tt> - Sets the path or custom method that resources will be fetched from.
1026-
# * <tt>:params</tt> - Sets query and \prefix (nested URL) parameters.
1045+
# * <tt>:params</tt> - Sets query and \prefix (nested URL) parameters. Query keys are URL
1046+
# encoded using the resource's +query_format+ (the ActiveResource::Formats::UrlEncodedFormat by default).
10271047
#
10281048
# ==== Examples
10291049
# Person.find(1)
@@ -1109,6 +1129,8 @@ def all(*args)
11091129
WhereClause.new(self, *args)
11101130
end
11111131

1132+
# This is an alias for all. You can pass in all the same
1133+
# arguments to this method as you can to <tt>all</tt> and <tt>find(:all)</tt>
11121134
def where(clauses = {})
11131135
clauses = sanitize_forbidden_attributes(clauses)
11141136
raise ArgumentError, "expected a clauses Hash, got #{clauses.inspect}" unless clauses.is_a? Hash
@@ -1235,7 +1257,7 @@ def prefix_parameters
12351257

12361258
# Builds the query string for the request.
12371259
def query_string(options)
1238-
"?#{options.to_query}" unless options.nil? || options.empty?
1260+
"?#{query_format.encode(options)}" unless options.nil? || options.empty?
12391261
end
12401262

12411263
# split an option hash into two hashes, one containing the prefix options,

lib/active_resource/formats.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module ActiveResource
44
module Formats
55
autoload :XmlFormat, "active_resource/formats/xml_format"
66
autoload :JsonFormat, "active_resource/formats/json_format"
7+
autoload :UrlEncodedFormat, "active_resource/formats/url_encoded_format"
78

89
# Lookup the format class from a mime type reference symbol. Example:
910
#
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
require "active_support/core_ext/array/wrap"
4+
5+
module ActiveResource
6+
module Formats
7+
module UrlEncodedFormat
8+
extend self
9+
10+
# URL encode the parameters Hash
11+
def encode(params, options = nil)
12+
params.to_query
13+
end
14+
end
15+
end
16+
end

test/abstract_unit.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def setup_response
3232
@joe = { person: { id: 6, name: "Joe", likes_hats: true } }.to_json
3333
@people = { people: [ { person: { id: 1, name: "Matz" } }, { person: { id: 2, name: "David" } } ] }.to_json
3434
@people_david = { people: [ { person: { id: 2, name: "David" } } ] }.to_json
35+
@people_joe = { people: [ { id: 6, name: "Joe", likes_hats: true } ] }.to_json
3536
@addresses = { addresses: [ { address: { id: 1, street: "12345 Street", country: "Australia" } } ] }.to_json
3637
@post = { id: 1, title: "Hello World", body: "Lorem Ipsum" }.to_json
3738
@posts = [ { id: 1, title: "Hello World", body: "Lorem Ipsum" }, { id: 2, title: "Second Post", body: "Lorem Ipsum" } ].to_json

test/cases/finder_test.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@
99
require "fixtures/pet"
1010
require "active_support/core_ext/hash/conversions"
1111

12+
module CamelcaseUrlEncodedFormat
13+
extend ActiveResource::Formats::UrlEncodedFormat
14+
15+
def self.encode(params, options = nil)
16+
params = params.deep_transform_keys { |key| key.to_s.camelcase(:lower) }
17+
18+
super
19+
end
20+
end
21+
22+
class CamelcasePerson < Person
23+
self.query_format = CamelcaseUrlEncodedFormat
24+
end
25+
26+
class CamelcasePet < Pet
27+
self.query_format = CamelcaseUrlEncodedFormat
28+
end
29+
1230
class FinderTest < ActiveSupport::TestCase
1331
def setup
1432
setup_response # find me in abstract_unit
@@ -143,6 +161,15 @@ def test_where_with_clause_in
143161
assert_equal "David", people.first.name
144162
end
145163

164+
def test_where_with_clause_in_custom_query_format
165+
ActiveResource::HttpMock.respond_to { |m| m.get "/camelcase_people.json?likesHats=true", {}, @people_joe }
166+
people = CamelcasePerson.where(likes_hats: true)
167+
assert_equal 1, people.size
168+
assert_kind_of CamelcasePerson, people.first
169+
assert_equal "Joe", people.first.name
170+
assert_predicate people.first, :likes_hats
171+
end
172+
146173
def test_where_with_invalid_clauses
147174
error = assert_raise(ArgumentError) { Person.where(nil) }
148175
assert_equal "expected a clauses Hash, got nil", error.message
@@ -230,6 +257,18 @@ def test_find_all_by_from_with_prefix
230257
assert_equal ({ person_id: 1 }), pets.second.prefix_options
231258
end
232259

260+
def test_find_all_with_prefix_and_custom_query_format
261+
ActiveResource::HttpMock.respond_to { |m| m.get "/people/1/camelcase_pets.json?queryParam=1", {}, @pets }
262+
263+
pets = CamelcasePet.find(:all, params: { person_id: 1, query_param: 1 })
264+
assert_equal 2, pets.size
265+
assert_equal "Max", pets.first.name
266+
assert_equal ({ person_id: 1 }), pets.first.prefix_options
267+
268+
assert_equal "Daisy", pets.second.name
269+
assert_equal ({ person_id: 1 }), pets.second.prefix_options
270+
end
271+
233272
def test_find_all_by_symbol_from
234273
ActiveResource::HttpMock.respond_to { |m| m.get "/people/managers.json", {}, @people_david }
235274

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
5+
class UrlEncodedFormatTest < ActiveSupport::TestCase
6+
test "#encode transforms a Hash into an application/x-www-form-urlencoded query string" do
7+
params = { "a" => 1, "b" => 2, "c" => [ 3, 4 ] }
8+
9+
encoded = ActiveResource::Formats::UrlEncodedFormat.encode(params)
10+
11+
assert_equal "a=1&b=2&c%5B%5D=3&c%5B%5D=4", encoded
12+
end
13+
end

0 commit comments

Comments
 (0)