jsonapi_compliable 0.4.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|