jsonapi_compliable 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Appraisals +4 -0
- data/Gemfile +2 -2
- data/gemfiles/rails_4.gemfile +4 -3
- data/gemfiles/rails_4.gemfile.lock +27 -36
- data/gemfiles/rails_5.gemfile +4 -3
- data/gemfiles/rails_5.gemfile.lock +27 -37
- data/jsonapi_compliable.gemspec +3 -7
- data/lib/jsonapi_compliable/adapters/abstract.rb +25 -0
- data/lib/jsonapi_compliable/adapters/active_record.rb +47 -0
- data/lib/jsonapi_compliable/adapters/active_record_sideloading.rb +119 -0
- data/lib/jsonapi_compliable/adapters/null.rb +21 -0
- data/lib/jsonapi_compliable/base.rb +43 -50
- data/lib/jsonapi_compliable/deserializable.rb +9 -4
- data/lib/jsonapi_compliable/extensions/extra_attribute.rb +1 -5
- data/lib/jsonapi_compliable/query.rb +153 -0
- data/lib/jsonapi_compliable/rails.rb +14 -0
- data/lib/jsonapi_compliable/resource.rb +191 -0
- data/lib/jsonapi_compliable/scope.rb +57 -0
- data/lib/jsonapi_compliable/{scope → scoping}/base.rb +5 -6
- data/lib/jsonapi_compliable/{scope → scoping}/default_filter.rb +3 -3
- data/lib/jsonapi_compliable/scoping/extra_fields.rb +25 -0
- data/lib/jsonapi_compliable/{scope → scoping}/filter.rb +4 -4
- data/lib/jsonapi_compliable/{scope → scoping}/filterable.rb +4 -4
- data/lib/jsonapi_compliable/{scope → scoping}/paginate.rb +6 -6
- data/lib/jsonapi_compliable/scoping/sort.rb +33 -0
- data/lib/jsonapi_compliable/sideload.rb +95 -0
- data/lib/jsonapi_compliable/stats/dsl.rb +7 -16
- data/lib/jsonapi_compliable/stats/payload.rb +6 -14
- data/lib/jsonapi_compliable/util/field_params.rb +6 -8
- data/lib/jsonapi_compliable/util/hash.rb +14 -0
- data/lib/jsonapi_compliable/util/include_params.rb +7 -18
- data/lib/jsonapi_compliable/util/render_options.rb +25 -0
- data/lib/jsonapi_compliable/version.rb +1 -1
- data/lib/jsonapi_compliable.rb +22 -13
- metadata +34 -70
- data/gemfiles/rails_3.gemfile +0 -9
- data/lib/jsonapi_compliable/dsl.rb +0 -90
- data/lib/jsonapi_compliable/scope/extra_fields.rb +0 -35
- data/lib/jsonapi_compliable/scope/sideload.rb +0 -25
- data/lib/jsonapi_compliable/scope/sort.rb +0 -29
- data/lib/jsonapi_compliable/util/pagination.rb +0 -11
- data/lib/jsonapi_compliable/util/scoping.rb +0 -20
@@ -6,65 +6,56 @@ module JsonapiCompliable
|
|
6
6
|
MAX_PAGE_SIZE = 1_000
|
7
7
|
|
8
8
|
included do
|
9
|
-
|
10
|
-
|
9
|
+
class << self
|
10
|
+
attr_accessor :_jsonapi_compliable
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
13
|
+
def self.inherited(klass)
|
14
|
+
super
|
15
|
+
klass._jsonapi_compliable = Class.new(_jsonapi_compliable)
|
16
|
+
end
|
14
17
|
end
|
15
18
|
|
16
|
-
def
|
17
|
-
|
19
|
+
def resource
|
20
|
+
@resource ||= self.class._jsonapi_compliable.new
|
18
21
|
end
|
19
22
|
|
20
|
-
def
|
21
|
-
|
23
|
+
def resource!
|
24
|
+
@resource = self.class._jsonapi_compliable.new
|
22
25
|
end
|
23
26
|
|
24
|
-
def
|
25
|
-
|
27
|
+
def query
|
28
|
+
@query ||= Query.new(resource, params)
|
26
29
|
end
|
27
30
|
|
28
|
-
def
|
29
|
-
|
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
|
31
|
+
def query_hash
|
32
|
+
@query_hash ||= query.to_hash[resource.type]
|
43
33
|
end
|
44
34
|
|
45
|
-
|
46
|
-
|
35
|
+
# TODO pass controller and action name here to guard
|
36
|
+
def wrap_context
|
37
|
+
if self.class._jsonapi_compliable
|
38
|
+
resource.with_context(self, action_name.to_sym) do
|
39
|
+
yield
|
40
|
+
end
|
41
|
+
end
|
47
42
|
end
|
48
43
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
44
|
+
def jsonapi_scope(scope, opts = {})
|
45
|
+
resource.build_scope(scope, query, opts)
|
46
|
+
end
|
47
|
+
|
48
|
+
def perform_render_jsonapi(opts)
|
49
|
+
JSONAPI::Serializable::Renderer.render(opts.delete(:jsonapi), opts)
|
52
50
|
end
|
53
51
|
|
54
52
|
def render_jsonapi(scope, opts = {})
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
options.merge!(opts)
|
62
|
-
options[:meta][:stats] = Stats::Payload.new(self, scoped).generate if params[:stats]
|
63
|
-
options[:expose] ||= {}
|
64
|
-
options[:expose][:context] = self
|
65
|
-
options[:expose][:extra_fields] = Util::FieldParams.fieldset(params, :extra_fields) if params[:extra_fields]
|
66
|
-
|
67
|
-
render(options)
|
53
|
+
scope = jsonapi_scope(scope) unless opts[:scope] == false || scope.is_a?(JsonapiCompliable::Scope)
|
54
|
+
opts = default_jsonapi_render_options.merge(opts)
|
55
|
+
opts = Util::RenderOptions.generate(scope, query_hash, opts)
|
56
|
+
opts[:expose][:context] = self
|
57
|
+
opts[:include] = forced_includes if force_includes?
|
58
|
+
perform_render_jsonapi(opts)
|
68
59
|
end
|
69
60
|
|
70
61
|
# render_jsonapi(foo) equivalent to
|
@@ -74,11 +65,11 @@ module JsonapiCompliable
|
|
74
65
|
end
|
75
66
|
end
|
76
67
|
|
68
|
+
# Legacy
|
77
69
|
# TODO: This nastiness likely goes away once jsonapi standardizes
|
78
70
|
# a spec for nested relationships.
|
79
71
|
# See: https://github.com/json-api/json-api/issues/1089
|
80
72
|
def forced_includes(data = nil)
|
81
|
-
return unless force_includes?
|
82
73
|
data = raw_params[:data] unless data
|
83
74
|
|
84
75
|
{}.tap do |forced|
|
@@ -95,21 +86,23 @@ module JsonapiCompliable
|
|
95
86
|
end
|
96
87
|
end
|
97
88
|
|
89
|
+
# Legacy
|
98
90
|
def force_includes?
|
99
91
|
%w(PUT PATCH POST).include?(request.method) and
|
100
92
|
raw_params.try(:[], :data).try(:[], :relationships).present?
|
101
93
|
end
|
102
94
|
|
103
95
|
module ClassMethods
|
104
|
-
def jsonapi(&blk)
|
105
|
-
if
|
106
|
-
|
107
|
-
self._jsonapi_compliable = dsl
|
96
|
+
def jsonapi(resource: nil, &blk)
|
97
|
+
if resource
|
98
|
+
self._jsonapi_compliable = resource
|
108
99
|
else
|
109
|
-
|
100
|
+
if !self._jsonapi_compliable
|
101
|
+
self._jsonapi_compliable = Class.new(JsonapiCompliable::Resource)
|
102
|
+
end
|
110
103
|
end
|
111
104
|
|
112
|
-
self._jsonapi_compliable.
|
105
|
+
self._jsonapi_compliable.class_eval(&blk) if blk
|
113
106
|
end
|
114
107
|
end
|
115
108
|
end
|
@@ -113,10 +113,15 @@ module JsonapiCompliable
|
|
113
113
|
|
114
114
|
def deserialize_jsonapi!
|
115
115
|
self.raw_params = self.params.deep_dup
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
116
|
+
|
117
|
+
if defined?(::Rails) && (is_a?(ActionController::Base) || (defined?(ActionController::API) && is_a?(ActionController::API)))
|
118
|
+
hash = params.to_unsafe_h
|
119
|
+
hash = hash.with_indifferent_access if ::Rails::VERSION::MAJOR == 4
|
120
|
+
deserialized = Deserialization.new(hash).deserialize
|
121
|
+
self.params = ActionController::Parameters.new(deserialized)
|
122
|
+
else
|
123
|
+
self.params = Deserialization.new(params).deserialize
|
124
|
+
end
|
120
125
|
end
|
121
126
|
end
|
122
127
|
end
|
@@ -14,11 +14,7 @@ module JsonapiCompliable
|
|
14
14
|
next false unless instance_eval(&options[:if])
|
15
15
|
end
|
16
16
|
|
17
|
-
|
18
|
-
@extra_fields[jsonapi_type].include?(name)
|
19
|
-
else
|
20
|
-
false
|
21
|
-
end
|
17
|
+
@extra_fields[@_type] && @extra_fields[@_type].include?(name)
|
22
18
|
}
|
23
19
|
|
24
20
|
attribute name, if: allow_field, &blk
|
@@ -0,0 +1,153 @@
|
|
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, :resource
|
6
|
+
|
7
|
+
def self.default_hash
|
8
|
+
{
|
9
|
+
filter: {},
|
10
|
+
sort: [],
|
11
|
+
page: {},
|
12
|
+
include: {},
|
13
|
+
stats: {},
|
14
|
+
fields: {},
|
15
|
+
extra_fields: {}
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(resource, params)
|
20
|
+
@resource = resource
|
21
|
+
@params = params
|
22
|
+
end
|
23
|
+
|
24
|
+
def include_directive
|
25
|
+
@include_directive ||= JSONAPI::IncludeDirective.new(params[:include])
|
26
|
+
end
|
27
|
+
|
28
|
+
def include_hash
|
29
|
+
@include_hash ||= include_directive.to_hash
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_requested_association_names
|
33
|
+
@all_requested_association_names ||= Util::Hash.keys(include_hash)
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_hash
|
37
|
+
hash = { resource.type => self.class.default_hash }
|
38
|
+
|
39
|
+
all_requested_association_names.each do |name|
|
40
|
+
hash[name] = self.class.default_hash
|
41
|
+
end
|
42
|
+
|
43
|
+
fields = parse_fields({}, :fields)
|
44
|
+
extra_fields = parse_fields({}, :extra_fields)
|
45
|
+
hash.each_pair do |type, query_hash|
|
46
|
+
hash[type][:fields] = fields
|
47
|
+
hash[type][:extra_fields] = extra_fields
|
48
|
+
end
|
49
|
+
|
50
|
+
parse_filter(hash)
|
51
|
+
parse_sort(hash)
|
52
|
+
parse_pagination(hash)
|
53
|
+
parse_include(hash, include_hash)
|
54
|
+
parse_stats(hash)
|
55
|
+
|
56
|
+
hash
|
57
|
+
end
|
58
|
+
|
59
|
+
def zero_results?
|
60
|
+
!@params[:page].nil? &&
|
61
|
+
!@params[:page][:size].nil? &&
|
62
|
+
@params[:page][:size].to_i == 0
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def association?(name)
|
68
|
+
resource.association_names.include?(name)
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_include(memo, incl_hash, namespace = nil)
|
72
|
+
namespace ||= resource.type
|
73
|
+
|
74
|
+
memo[namespace][:include] = incl_hash
|
75
|
+
incl_hash.each_pair do |key, sub_hash|
|
76
|
+
memo[key][:include] = parse_include(memo, sub_hash, key)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def parse_stats(hash)
|
81
|
+
if params[:stats]
|
82
|
+
params[:stats].each_pair do |namespace, calculations|
|
83
|
+
if namespace == resource.type || association?(namespace)
|
84
|
+
calculations.each_pair do |name, calcs|
|
85
|
+
hash[namespace][:stats][name] = calcs.split(',').map(&:to_sym)
|
86
|
+
end
|
87
|
+
else
|
88
|
+
hash[resource.type][:stats][namespace] = calculations.split(',').map(&:to_sym)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def parse_fields(hash, type)
|
95
|
+
field_params = Util::FieldParams.parse(params[type])
|
96
|
+
hash[type] = field_params
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_filter(hash)
|
100
|
+
if filter = params[:filter]
|
101
|
+
filter.each_pair do |key, value|
|
102
|
+
key = key.to_sym
|
103
|
+
|
104
|
+
if association?(key)
|
105
|
+
hash[key][:filter].merge!(value)
|
106
|
+
else
|
107
|
+
hash[resource.type][:filter][key] = value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_sort(hash)
|
114
|
+
if sort = params[:sort]
|
115
|
+
sorts = sort.split(',')
|
116
|
+
sorts.each do |s|
|
117
|
+
if s.include?('.')
|
118
|
+
type, attr = s.split('.')
|
119
|
+
if type.starts_with?('-')
|
120
|
+
type = type.sub('-', '')
|
121
|
+
attr = "-#{attr}"
|
122
|
+
end
|
123
|
+
|
124
|
+
hash[type.to_sym][:sort] << sort_attr(attr)
|
125
|
+
else
|
126
|
+
hash[resource.type][:sort] << sort_attr(s)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def parse_pagination(hash)
|
133
|
+
if pagination = params[:page]
|
134
|
+
pagination.each_pair do |key, value|
|
135
|
+
key = key.to_sym
|
136
|
+
|
137
|
+
if [:number, :size].include?(key)
|
138
|
+
hash[resource.type][:page][key] = value.to_i
|
139
|
+
else
|
140
|
+
hash[key][:page] = { number: value[:number].to_i, size: value[:size].to_i }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def sort_attr(attr)
|
147
|
+
value = attr.starts_with?('-') ? :desc : :asc
|
148
|
+
key = attr.sub('-', '').to_sym
|
149
|
+
|
150
|
+
{ key => value }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'jsonapi/rails'
|
2
|
+
|
3
|
+
module JsonapiCompliable
|
4
|
+
module Rails
|
5
|
+
def self.included(klass)
|
6
|
+
klass.send(:include, Base)
|
7
|
+
|
8
|
+
klass.class_eval do
|
9
|
+
around_action :wrap_context
|
10
|
+
alias_method :perform_render_jsonapi, :render
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
module JsonapiCompliable
|
2
|
+
class Resource
|
3
|
+
attr_reader :filters,
|
4
|
+
:default_filters,
|
5
|
+
:default_sort,
|
6
|
+
:default_page_size,
|
7
|
+
:default_page_number,
|
8
|
+
:sorting,
|
9
|
+
:stats,
|
10
|
+
:sideload_whitelist,
|
11
|
+
:pagination,
|
12
|
+
:extra_fields,
|
13
|
+
:sideloading,
|
14
|
+
:adapter,
|
15
|
+
:type,
|
16
|
+
:context
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :config
|
20
|
+
end
|
21
|
+
|
22
|
+
delegate :sideload, to: :sideloading
|
23
|
+
|
24
|
+
# Incorporate custom adapter methods
|
25
|
+
def self.method_missing(meth, *args, &blk)
|
26
|
+
if sideloading.respond_to?(meth)
|
27
|
+
sideloading.send(meth, *args, &blk)
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.inherited(klass)
|
34
|
+
klass.config = self.config.deep_dup
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.sideloading
|
38
|
+
config[:sideloading] ||= Sideload.new(:base, resource: self)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.sideload_whitelist(whitelist)
|
42
|
+
config[:sideload_whitelist] = JSONAPI::IncludeDirective.new(whitelist).to_hash
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.allow_filter(name, *args, &blk)
|
46
|
+
opts = args.extract_options!
|
47
|
+
aliases = [name, opts[:aliases]].flatten.compact
|
48
|
+
config[:filters][name.to_sym] = {
|
49
|
+
aliases: aliases,
|
50
|
+
if: opts[:if],
|
51
|
+
filter: blk
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.allow_stat(symbol_or_hash, &blk)
|
56
|
+
dsl = Stats::DSL.new(config[:adapter], symbol_or_hash)
|
57
|
+
dsl.instance_eval(&blk) if blk
|
58
|
+
config[:stats][dsl.name] = dsl
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.default_filter(name, &blk)
|
62
|
+
config[:default_filters][name.to_sym] = {
|
63
|
+
filter: blk
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.sort(&blk)
|
68
|
+
config[:sorting] = blk
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.paginate(&blk)
|
72
|
+
config[:pagination] = blk
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.extra_field(name, &blk)
|
76
|
+
config[:extra_fields][name] = blk
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.use_adapter(klass)
|
80
|
+
config[:adapter] = klass.new
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.default_sort(val)
|
84
|
+
config[:default_sort] = val
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.type(value = nil)
|
88
|
+
config[:type] = value
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.default_page_number(val)
|
92
|
+
config[:default_page_number] = val
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.default_page_size(val)
|
96
|
+
config[:default_page_size] = val
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.config
|
100
|
+
@config ||= begin
|
101
|
+
{
|
102
|
+
sideload_whitelist: {},
|
103
|
+
filters: {},
|
104
|
+
default_filters: {},
|
105
|
+
extra_fields: {},
|
106
|
+
stats: {},
|
107
|
+
sorting: nil,
|
108
|
+
pagination: nil,
|
109
|
+
adapter: Adapters::Abstract.new
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def copy
|
115
|
+
self.class.new(@config.deep_dup)
|
116
|
+
end
|
117
|
+
|
118
|
+
def initialize(config = nil)
|
119
|
+
config = config || self.class.config.deep_dup
|
120
|
+
set_config(config)
|
121
|
+
end
|
122
|
+
|
123
|
+
def set_config(config)
|
124
|
+
@config = config
|
125
|
+
config.each_pair do |key, value|
|
126
|
+
instance_variable_set(:"@#{key}", value)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def with_context(object, namespace = nil)
|
131
|
+
begin
|
132
|
+
prior = context
|
133
|
+
@context = { object: object, namespace: namespace }
|
134
|
+
yield
|
135
|
+
ensure
|
136
|
+
@context = prior
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def context
|
141
|
+
@context || {}
|
142
|
+
end
|
143
|
+
|
144
|
+
def build_scope(base, query, opts = {})
|
145
|
+
Scope.new(base, self, query, opts)
|
146
|
+
end
|
147
|
+
|
148
|
+
def association_names
|
149
|
+
@association_names ||= begin
|
150
|
+
if sideloading
|
151
|
+
Util::Hash.keys(sideloading.to_hash[:base])
|
152
|
+
else
|
153
|
+
[]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def allowed_sideloads(namespace = nil)
|
159
|
+
return {} unless sideloading
|
160
|
+
|
161
|
+
namespace ||= context[:namespace]
|
162
|
+
sideloads = sideloading.to_hash[:base]
|
163
|
+
if !sideload_whitelist.empty? && namespace
|
164
|
+
sideloads = Util::IncludeParams.scrub(sideloads, sideload_whitelist[namespace])
|
165
|
+
end
|
166
|
+
sideloads
|
167
|
+
end
|
168
|
+
|
169
|
+
def stat(attribute, calculation)
|
170
|
+
stats_dsl = stats[attribute] || stats[attribute.to_sym]
|
171
|
+
raise Errors::StatNotFound.new(attribute, calculation) unless stats_dsl
|
172
|
+
stats_dsl.calculation(calculation)
|
173
|
+
end
|
174
|
+
|
175
|
+
def default_sort
|
176
|
+
@default_sort || [{ id: :asc }]
|
177
|
+
end
|
178
|
+
|
179
|
+
def default_page_number
|
180
|
+
@default_page_number || 1
|
181
|
+
end
|
182
|
+
|
183
|
+
def default_page_size
|
184
|
+
@default_page_size || 20
|
185
|
+
end
|
186
|
+
|
187
|
+
def type
|
188
|
+
@type || :undefined_jsonapi_type
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module JsonapiCompliable
|
2
|
+
class Scope
|
3
|
+
def initialize(object, resource, query, opts = {})
|
4
|
+
@object = object
|
5
|
+
@resource = resource
|
6
|
+
@query = query
|
7
|
+
|
8
|
+
# Namespace for the 'outer' or 'main' resource is its type
|
9
|
+
# For its relationships, its the relationship name
|
10
|
+
# IOW when hitting /states, it's resource type 'states
|
11
|
+
# when hitting /authors?include=state its 'state'
|
12
|
+
@namespace = opts.delete(:namespace) || resource.type
|
13
|
+
|
14
|
+
apply_scoping(opts)
|
15
|
+
end
|
16
|
+
|
17
|
+
def resolve_stats
|
18
|
+
Stats::Payload.new(@resource, query_hash, @unpaginated_object).generate
|
19
|
+
end
|
20
|
+
|
21
|
+
def resolve
|
22
|
+
if @query.zero_results?
|
23
|
+
[]
|
24
|
+
else
|
25
|
+
resolved = @object
|
26
|
+
# TODO - configurable resolve function
|
27
|
+
resolved = @object.to_a if @object.is_a?(ActiveRecord::Relation)
|
28
|
+
sideload(resolved, query_hash[:include]) if query_hash[:include]
|
29
|
+
resolved
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def query_hash
|
34
|
+
@query_hash ||= @query.to_hash[@namespace]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def sideload(results, includes)
|
40
|
+
includes.each_pair do |name, nested|
|
41
|
+
if @resource.allowed_sideloads.has_key?(name)
|
42
|
+
sideload = @resource.sideload(name)
|
43
|
+
sideload.resolve(results, @query)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def apply_scoping(opts)
|
49
|
+
@object = JsonapiCompliable::Scoping::DefaultFilter.new(@resource, query_hash, @object).apply
|
50
|
+
@object = JsonapiCompliable::Scoping::Filter.new(@resource, query_hash, @object).apply unless opts[:filter] == false
|
51
|
+
@object = JsonapiCompliable::Scoping::ExtraFields.new(@resource, query_hash, @object).apply unless opts[:extra_fields] == false
|
52
|
+
@object = JsonapiCompliable::Scoping::Sort.new(@resource, query_hash, @object).apply unless opts[:sort] == false
|
53
|
+
@unpaginated_object = @object
|
54
|
+
@object = JsonapiCompliable::Scoping::Paginate.new(@resource, query_hash, @object).apply unless opts[:paginate] == false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,12 +1,11 @@
|
|
1
1
|
module JsonapiCompliable
|
2
|
-
module
|
2
|
+
module Scoping
|
3
3
|
class Base
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :resource, :query_hash
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@params = controller.params
|
6
|
+
def initialize(resource, query_hash, scope)
|
7
|
+
@query_hash = query_hash
|
8
|
+
@resource = resource
|
10
9
|
@scope = scope
|
11
10
|
end
|
12
11
|
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module JsonapiCompliable
|
2
|
-
class
|
3
|
-
include
|
2
|
+
class Scoping::DefaultFilter < Scoping::Base
|
3
|
+
include Scoping::Filterable
|
4
4
|
|
5
5
|
def apply
|
6
|
-
|
6
|
+
resource.default_filters.each_pair do |name, opts|
|
7
7
|
next if overridden?(name)
|
8
8
|
@scope = opts[:filter].call(@scope)
|
9
9
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module JsonapiCompliable
|
2
|
+
class Scoping::ExtraFields < Scoping::Base
|
3
|
+
def apply
|
4
|
+
each_extra_field do |callable|
|
5
|
+
@scope = callable.call(@scope)
|
6
|
+
end
|
7
|
+
|
8
|
+
@scope
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def each_extra_field
|
14
|
+
resource.extra_fields.each_pair do |name, callable|
|
15
|
+
if extra_fields.include?(name)
|
16
|
+
yield callable
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def extra_fields
|
22
|
+
query_hash[:extra_fields][resource.type] || []
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module JsonapiCompliable
|
2
|
-
class
|
3
|
-
include
|
2
|
+
class Scoping::Filter < Scoping::Base
|
3
|
+
include Scoping::Filterable
|
4
4
|
|
5
5
|
def apply
|
6
6
|
each_filter do |filter, value|
|
@@ -14,7 +14,7 @@ module JsonapiCompliable
|
|
14
14
|
if custom_scope = filter.values.first[:filter]
|
15
15
|
custom_scope.call(@scope, value)
|
16
16
|
else
|
17
|
-
@scope
|
17
|
+
resource.adapter.filter(@scope, filter.keys.first, value)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -24,7 +24,7 @@ module JsonapiCompliable
|
|
24
24
|
filter_param.each_pair do |param_name, param_value|
|
25
25
|
filter = find_filter!(param_name.to_sym)
|
26
26
|
value = param_value
|
27
|
-
value = value.split(',') if value.include?(',')
|
27
|
+
value = value.split(',') if value.is_a?(String) && value.include?(',')
|
28
28
|
yield filter, value
|
29
29
|
end
|
30
30
|
end
|