Skip to content

Commit ee5c56a

Browse files
author
Lee Richmond
committed
Refactor scope logic
* Introduce Query class which will parse incoming params into something we can work with. * Introduce Scope so we can call resolve and handle association loading * General refactoring
1 parent a4bd278 commit ee5c56a

40 files changed

+882
-404
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ source "https://rubygems.org"
44
gemspec
55

66
group :test do
7+
gem 'pry'
8+
gem 'pry-byebug', platform: [:mri]
79
gem 'appraisal'
810
gem 'guard'
911
gem 'guard-rspec'

lib/jsonapi_compliable.rb

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@
33
require "jsonapi_compliable/version"
44
require "jsonapi_compliable/errors"
55
require "jsonapi_compliable/dsl"
6-
require "jsonapi_compliable/scope/base"
7-
require "jsonapi_compliable/scope/sort"
8-
require "jsonapi_compliable/scope/paginate"
9-
require "jsonapi_compliable/scope/sideload"
10-
require "jsonapi_compliable/scope/extra_fields"
11-
require "jsonapi_compliable/scope/filterable"
12-
require "jsonapi_compliable/scope/default_filter"
13-
require "jsonapi_compliable/scope/filter"
6+
require "jsonapi_compliable/query"
7+
require "jsonapi_compliable/scope"
8+
require "jsonapi_compliable/scoping/base"
9+
require "jsonapi_compliable/scoping/sort"
10+
require "jsonapi_compliable/scoping/paginate"
11+
require "jsonapi_compliable/scoping/sideload"
12+
require "jsonapi_compliable/scoping/extra_fields"
13+
require "jsonapi_compliable/scoping/filterable"
14+
require "jsonapi_compliable/scoping/default_filter"
15+
require "jsonapi_compliable/scoping/filter"
1416
require "jsonapi_compliable/stats/dsl"
1517
require "jsonapi_compliable/stats/payload"
1618
require "jsonapi_compliable/util/include_params"
1719
require "jsonapi_compliable/util/field_params"
18-
require "jsonapi_compliable/util/scoping"
19-
require "jsonapi_compliable/util/pagination"
20+
require "jsonapi_compliable/util/hash"
2021
require "jsonapi_compliable/extensions/extra_attribute"
2122
require "jsonapi_compliable/extensions/boolean_attribute"
2223

lib/jsonapi_compliable/base.rb

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,60 +9,45 @@ module Base
99
class_attribute :_jsonapi_compliable
1010
attr_reader :_jsonapi_scope
1111

12-
before_action :parse_fieldsets!
13-
after_action :reset_scope_flag
12+
around_action :wrap_context
1413
end
1514

16-
def default_page_number
17-
1
18-
end
19-
20-
def default_page_size
21-
20
22-
end
23-
24-
def default_sort
25-
'id'
15+
# TODO pass controller and action name here to guard
16+
def wrap_context
17+
_jsonapi_compliable.with_context(self, action_name.to_sym) do
18+
yield
19+
end
2620
end
2721

28-
def jsonapi_scope(scope,
29-
filter: true,
30-
includes: true,
31-
paginate: true,
32-
extra_fields: true,
33-
sort: true)
34-
scope = JsonapiCompliable::Scope::DefaultFilter.new(self, scope).apply
35-
scope = JsonapiCompliable::Scope::Filter.new(self, scope).apply if filter
36-
scope = JsonapiCompliable::Scope::ExtraFields.new(self, scope).apply if extra_fields
37-
scope = JsonapiCompliable::Scope::Sideload.new(self, scope).apply if includes
38-
scope = JsonapiCompliable::Scope::Sort.new(self, scope).apply if sort
39-
# This is set before pagination so it can be re-used for stats
40-
@_jsonapi_scope = scope
41-
scope = JsonapiCompliable::Scope::Paginate.new(self, scope).apply if paginate
42-
scope
22+
def jsonapi_scope(scope, opts = {})
23+
query = Query.new(_jsonapi_compliable, params)
24+
_jsonapi_compliable.build_scope(scope, query, opts)
4325
end
4426

45-
def reset_scope_flag
46-
@_jsonapi_scope = nil
47-
end
27+
# TODO: refactor
28+
def render_jsonapi(scope, opts = {})
29+
query = Query.new(_jsonapi_compliable, params)
30+
query_hash = query.to_hash[_jsonapi_compliable.type]
4831

49-
def parse_fieldsets!
50-
Util::FieldParams.parse!(params, :fields)
51-
Util::FieldParams.parse!(params, :extra_fields)
52-
end
32+
scoped = scope
33+
scoped = jsonapi_scope(scoped) unless opts[:scope] == false || scoped.is_a?(JsonapiCompliable::Scope)
34+
resolved = scoped.respond_to?(:resolve) ? scoped.resolve : scoped
5335

54-
def render_jsonapi(scope, opts = {})
55-
scoped = Util::Scoping.apply?(self, scope, opts.delete(:scope)) ? jsonapi_scope(scope) : scope
5636
options = default_jsonapi_render_options
57-
options[:include] = forced_includes || Util::IncludeParams.scrub(self)
58-
options[:jsonapi] = JsonapiCompliable::Util::Pagination.zero?(params) ? [] : scoped
59-
options[:fields] = Util::FieldParams.fieldset(params, :fields) if params[:fields]
37+
options[:include] = forced_includes || Util::IncludeParams.scrub(query_hash[:include], _jsonapi_compliable.allowed_sideloads)
38+
options[:jsonapi] = resolved
39+
options[:fields] = query.fieldsets
6040
options[:meta] ||= {}
6141
options.merge!(opts)
62-
options[:meta][:stats] = Stats::Payload.new(self, scoped).generate if params[:stats]
42+
43+
if scoped.respond_to?(:resolve_stats)
44+
stats = scoped.resolve_stats
45+
options[:meta][:stats] = stats unless stats.empty?
46+
end
47+
6348
options[:expose] ||= {}
6449
options[:expose][:context] = self
65-
options[:expose][:extra_fields] = Util::FieldParams.fieldset(params, :extra_fields) if params[:extra_fields]
50+
options[:expose][:extra_fields] = query_hash[:extra_fields]
6651

6752
render(options)
6853
end
@@ -74,6 +59,7 @@ def default_jsonapi_render_options
7459
end
7560
end
7661

62+
# Legacy
7763
# TODO: This nastiness likely goes away once jsonapi standardizes
7864
# a spec for nested relationships.
7965
# See: https://github.com/json-api/json-api/issues/1089
@@ -95,6 +81,7 @@ def forced_includes(data = nil)
9581
end
9682
end
9783

84+
# Legacy
9885
def force_includes?
9986
%w(PUT PATCH POST).include?(request.method) and
10087
raw_params.try(:[], :data).try(:[], :relationships).present?

lib/jsonapi_compliable/dsl.rb

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class DSL
88
:stats,
99
:pagination
1010

11+
attr_reader :context
12+
1113
def initialize
1214
clear!
1315
end
@@ -32,6 +34,73 @@ def clear!
3234
@stats = {}
3335
@sorting = nil
3436
@pagination = nil
37+
@context = {}
38+
end
39+
40+
def with_context(object, namespace = nil)
41+
begin
42+
prior = @context
43+
@context = { object: object, namespace: namespace }
44+
yield
45+
ensure
46+
@context = prior
47+
end
48+
end
49+
50+
def build_scope(base, query, opts = {})
51+
Scope.new(base, self, query, opts)
52+
end
53+
54+
def default_sort(val = nil)
55+
if val
56+
@default_sort = val
57+
else
58+
@default_sort || [{ id: :asc }]
59+
end
60+
end
61+
62+
def default_page_number(val = nil)
63+
if val
64+
@default_page_number = val
65+
else
66+
@default_page_number || 1
67+
end
68+
end
69+
70+
def default_page_size(val = nil)
71+
if val
72+
@default_page_size = val
73+
else
74+
@default_page_size || 20
75+
end
76+
end
77+
78+
def association_names
79+
@association_names ||= begin
80+
if whitelist = @sideloads[:whitelist]
81+
Util::Hash.keys(whitelist.to_hash.values.reduce(&:merge))
82+
else
83+
[]
84+
end
85+
end
86+
end
87+
88+
def allowed_sideloads
89+
return {} if @sideloads.empty?
90+
91+
if namespace = @context[:namespace]
92+
@sideloads[:whitelist][namespace].to_hash
93+
else
94+
@sideloads[:whitelist].to_hash.values.reduce(&:merge)
95+
end
96+
end
97+
98+
def type(value = nil)
99+
if value
100+
@type = value
101+
else
102+
@type || :undefined_jsonapi_type
103+
end
35104
end
36105

37106
def includes(whitelist: nil, &blk)
@@ -73,12 +142,8 @@ def paginate(&blk)
73142
@pagination = blk
74143
end
75144

76-
def extra_field(field, &blk)
77-
@extra_fields[field.keys.first] ||= []
78-
@extra_fields[field.keys.first] << {
79-
name: field.values.first,
80-
proc: blk
81-
}
145+
def extra_field(name, &blk)
146+
@extra_fields[name] = blk
82147
end
83148

84149
def stat(attribute, calculation)

lib/jsonapi_compliable/extensions/extra_attribute.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ def extra_attribute(name, options = {}, &blk)
1414
next false unless instance_eval(&options[:if])
1515
end
1616

17-
if @extra_fields && @extra_fields[jsonapi_type]
18-
@extra_fields[jsonapi_type].include?(name)
19-
else
20-
false
21-
end
17+
@extra_fields.include?(name)
2218
}
2319

2420
attribute name, if: allow_field, &blk

lib/jsonapi_compliable/query.rb

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# TODO: refactor - code could be better but it's a one-time thing.
2+
3+
module JsonapiCompliable
4+
class Query
5+
attr_reader :params, :dsl
6+
7+
def self.default_hash
8+
{
9+
filter: {},
10+
sort: [],
11+
page: {},
12+
include: {},
13+
extra_fields: [],
14+
fields: [],
15+
stats: {}
16+
}
17+
end
18+
19+
def initialize(dsl, params)
20+
@dsl = dsl
21+
@params = params
22+
end
23+
24+
def to_hash
25+
hash = { dsl.type => self.class.default_hash }
26+
dsl.association_names.each do |name|
27+
hash[name] = self.class.default_hash.except(:include)
28+
end
29+
30+
parse_fields(hash, :fields)
31+
parse_fields(hash, :extra_fields)
32+
parse_filter(hash)
33+
parse_sort(hash)
34+
parse_pagination(hash)
35+
parse_include(hash)
36+
parse_stats(hash)
37+
38+
hash
39+
end
40+
41+
# TODO: test
42+
def fieldsets
43+
{}.tap do |fs|
44+
to_hash.each_pair do |namespace, query_hash|
45+
if query_hash[:fields] and !query_hash[:fields].empty?
46+
fs[namespace] = query_hash[:fields]
47+
end
48+
end
49+
end
50+
end
51+
52+
def zero_results?
53+
!@params[:page].nil? &&
54+
!@params[:page][:size].nil? &&
55+
@params[:page][:size].to_i == 0
56+
end
57+
58+
private
59+
60+
def association?(name)
61+
dsl.association_names.include?(name)
62+
end
63+
64+
# TODO: maybe walk the graph and apply to all
65+
def parse_include(hash)
66+
hash[dsl.type][:include] = JSONAPI::IncludeDirective.new(params[:include] || {}).to_hash
67+
end
68+
69+
def parse_stats(hash)
70+
if params[:stats]
71+
params[:stats].each_pair do |namespace, calculations|
72+
if namespace == dsl.type || association?(namespace)
73+
calculations.each_pair do |name, calcs|
74+
hash[namespace][:stats][name] = calcs.split(',').map(&:to_sym)
75+
end
76+
else
77+
hash[dsl.type][:stats][namespace] = calculations.split(',').map(&:to_sym)
78+
end
79+
end
80+
end
81+
end
82+
83+
def parse_fields(hash, type)
84+
field_params = Util::FieldParams.parse(params[type])
85+
field_params.each_pair do |namespace, fields|
86+
hash[namespace][type] = fields
87+
end
88+
end
89+
90+
def parse_filter(hash)
91+
if filter = params[:filter]
92+
filter.each_pair do |key, value|
93+
key = key.to_sym
94+
95+
if association?(key)
96+
hash[key][:filter].merge!(value)
97+
else
98+
hash[dsl.type][:filter][key] = value
99+
end
100+
end
101+
end
102+
end
103+
104+
def parse_sort(hash)
105+
if sort = params[:sort]
106+
sorts = sort.split(',')
107+
sorts.each do |s|
108+
if s.include?('.')
109+
type, attr = s.split('.')
110+
if type.starts_with?('-')
111+
type = type.sub('-', '')
112+
attr = "-#{attr}"
113+
end
114+
115+
hash[type.to_sym][:sort] << sort_attr(attr)
116+
else
117+
hash[dsl.type][:sort] << sort_attr(s)
118+
end
119+
end
120+
end
121+
end
122+
123+
def parse_pagination(hash)
124+
if pagination = params[:page]
125+
pagination.each_pair do |key, value|
126+
key = key.to_sym
127+
128+
if [:number, :size].include?(key)
129+
hash[dsl.type][:page][key] = value.to_i
130+
else
131+
hash[key][:page] = { number: value[:number].to_i, size: value[:size].to_i }
132+
end
133+
end
134+
end
135+
end
136+
137+
def sort_attr(attr)
138+
value = attr.starts_with?('-') ? :desc : :asc
139+
key = attr.sub('-', '').to_sym
140+
141+
{ key => value }
142+
end
143+
end
144+
end

0 commit comments

Comments
 (0)