Skip to content

Commit fc06c87

Browse files
authored
Merge pull request #293 from gaorlov/idempotent_scoping
#292 #254 making scopes unmodifiable
2 parents b4e5804 + 9f31073 commit fc06c87

File tree

3 files changed

+73
-30
lines changed

3 files changed

+73
-30
lines changed

lib/json_api_client/paginating/paginator.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def next_page
8282
def params_for_uri(uri)
8383
return {} unless uri
8484
uri = Addressable::URI.parse(uri)
85-
uri.query_values || {}
85+
( uri.query_values || {} ).with_indifferent_access
8686
end
8787
end
8888
end

lib/json_api_client/query/builder.rb

+36-29
Original file line numberDiff line numberDiff line change
@@ -5,60 +5,55 @@ class Builder
55
attr_reader :klass
66
delegate :key_formatter, to: :klass
77

8-
def initialize(klass)
9-
@klass = klass
10-
@primary_key = nil
11-
@pagination_params = {}
12-
@path_params = {}
13-
@additional_params = {}
14-
@filters = {}
15-
@includes = []
16-
@orders = []
17-
@fields = []
8+
def initialize(klass, opts = {})
9+
@klass = klass
10+
@primary_key = opts.fetch( :primary_key, nil )
11+
@pagination_params = opts.fetch( :pagination_params, {} )
12+
@path_params = opts.fetch( :path_params, {} )
13+
@additional_params = opts.fetch( :additional_params, {} )
14+
@filters = opts.fetch( :filters, {} )
15+
@includes = opts.fetch( :includes, [] )
16+
@orders = opts.fetch( :orders, [] )
17+
@fields = opts.fetch( :fields, [] )
1818
end
1919

2020
def where(conditions = {})
2121
# pull out any path params here
22-
@path_params.merge!(conditions.slice(*klass.prefix_params))
23-
@filters.merge!(conditions.except(*klass.prefix_params))
24-
self
22+
path_conditions = conditions.slice(*klass.prefix_params)
23+
unpathed_conditions = conditions.except(*klass.prefix_params)
24+
25+
_new_scope( path_params: path_conditions, filters: unpathed_conditions )
2526
end
2627

2728
def order(*args)
28-
@orders += parse_orders(*args)
29-
self
29+
_new_scope( orders: parse_orders(*args) )
3030
end
3131

3232
def includes(*tables)
33-
@includes += parse_related_links(*tables)
34-
self
33+
_new_scope( includes: parse_related_links(*tables) )
3534
end
3635

3736
def select(*fields)
38-
@fields += parse_fields(*fields)
39-
self
37+
_new_scope( fields: parse_fields(*fields) )
4038
end
4139

4240
def paginate(conditions = {})
43-
scope = self
41+
scope = _new_scope
4442
scope = scope.page(conditions[:page]) if conditions[:page]
4543
scope = scope.per(conditions[:per_page]) if conditions[:per_page]
4644
scope
4745
end
4846

4947
def page(number)
50-
@pagination_params[ klass.paginator.page_param ] = number || 1
51-
self
48+
_new_scope( pagination_params: { klass.paginator.page_param => number || 1 } )
5249
end
5350

5451
def per(size)
55-
@pagination_params[ klass.paginator.per_page_param ] = size
56-
self
52+
_new_scope( pagination_params: { klass.paginator.per_page_param => size } )
5753
end
5854

5955
def with_params(more_params)
60-
@additional_params.merge!(more_params)
61-
self
56+
_new_scope( additional_params: more_params )
6257
end
6358

6459
def first
@@ -92,12 +87,12 @@ def to_a
9287
def find(args = {})
9388
case args
9489
when Hash
95-
where(args)
90+
scope = where(args)
9691
else
97-
@primary_key = args
92+
scope = _new_scope( primary_key: args )
9893
end
9994

100-
klass.requestor.get(params)
95+
klass.requestor.get(scope.params)
10196
end
10297

10398
def method_missing(method_name, *args, &block)
@@ -106,6 +101,18 @@ def method_missing(method_name, *args, &block)
106101

107102
private
108103

104+
def _new_scope( opts = {} )
105+
self.class.new( @klass,
106+
primary_key: opts.fetch( :primary_key, @primary_key ),
107+
pagination_params: @pagination_params.merge( opts.fetch( :pagination_params, {} ) ),
108+
path_params: @path_params.merge( opts.fetch( :path_params, {} ) ),
109+
additional_params: @additional_params.merge( opts.fetch( :additional_params, {} ) ),
110+
filters: @filters.merge( opts.fetch( :filters, {} ) ),
111+
includes: @includes + opts.fetch( :includes, [] ),
112+
orders: @orders + opts.fetch( :orders, [] ),
113+
fields: @fields + opts.fetch( :fields, [] ) )
114+
end
115+
109116
def path_params
110117
@path_params.empty? ? {} : {path: @path_params}
111118
end

test/unit/query_builder_test.rb

+36
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,40 @@ def test_can_specify_empty_string_filter_value
220220
Article.where(:'author.id' => '').to_a
221221
end
222222

223+
def test_scopes_are_nondestructive
224+
first_stub = stub_request(:get, "http://example.com/articles?page[page]=1&page[per_page]=1")
225+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: { data: [] }.to_json)
226+
227+
all_stub = stub_request(:get, "http://example.com/articles")
228+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: { data: [] }.to_json)
229+
230+
scope = Article.where()
231+
232+
scope.first
233+
scope.all
234+
235+
assert_requested first_stub, times: 1
236+
assert_requested all_stub, times: 1
237+
end
238+
239+
def test_find_with_args
240+
first_stub = stub_request(:get, "http://example.com/articles?filter[author.id]=foo")
241+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: { data: [] }.to_json)
242+
243+
all_stub = stub_request(:get, "http://example.com/articles")
244+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: { data: [] }.to_json)
245+
246+
find_stub = stub_request(:get, "http://example.com/articles/6")
247+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: { data: [] }.to_json)
248+
249+
scope = Article.where()
250+
251+
scope.find( "author.id" => "foo" )
252+
scope.find(6)
253+
scope.all
254+
255+
assert_requested first_stub, times: 1
256+
assert_requested all_stub, times: 1
257+
assert_requested find_stub, times: 1
258+
end
223259
end

0 commit comments

Comments
 (0)