Skip to content

Commit 26ceae8

Browse files
author
Lee Richmond
committed
Move to new adapters/sideloading pattern
1 parent d4ca615 commit 26ceae8

20 files changed

+575
-124
lines changed

lib/jsonapi_compliable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
require "jsonapi_compliable/errors"
55
require "jsonapi_compliable/resource"
66
require "jsonapi_compliable/query"
7+
require "jsonapi_compliable/sideload"
78
require "jsonapi_compliable/scope"
89
require "jsonapi_compliable/scoping/base"
910
require "jsonapi_compliable/scoping/sort"
1011
require "jsonapi_compliable/scoping/paginate"
11-
require "jsonapi_compliable/scoping/sideload"
1212
require "jsonapi_compliable/scoping/extra_fields"
1313
require "jsonapi_compliable/scoping/filterable"
1414
require "jsonapi_compliable/scoping/default_filter"

lib/jsonapi_compliable/adapters/abstract.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ def paginate(scope, number, size)
1616
def sideload(scope, includes)
1717
raise 'you must override #sideload in an adapter subclass'
1818
end
19+
20+
def sideloading_module
21+
Module.new
22+
end
1923
end
2024
end
2125
end

lib/jsonapi_compliable/adapters/active_record.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'jsonapi_compliable/adapters/active_record_sideloading'
2+
13
module JsonapiCompliable
24
module Adapters
35
class ActiveRecord < Abstract
@@ -36,6 +38,10 @@ def maximum(scope, attr)
3638
def minimum(scope, attr)
3739
scope.minimum(attr)
3840
end
41+
42+
def sideloading_module
43+
JsonapiCompliable::Adapters::ActiveRecordSideloading
44+
end
3945
end
4046
end
4147
end
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
module JsonapiCompliable
2+
module Adapters
3+
module ActiveRecordSideloading
4+
def has_many(association_name, scope:, resource:, foreign_key:, primary_key: :id, &blk)
5+
_scope = scope
6+
7+
allow_sideload association_name, resource: resource do
8+
scope do |parents|
9+
parent_ids = parents.map { |p| p.send(primary_key) }
10+
_scope.call.where(foreign_key => parent_ids)
11+
end
12+
13+
assign do |parents, children|
14+
parents.each do |parent|
15+
parent.association(association_name).loaded!
16+
relevant_children = children.select { |c| c.send(foreign_key) == parent.send(primary_key) }
17+
relevant_children.each do |c|
18+
parent.association(association_name).add_to_target(c, :skip_callbacks)
19+
end
20+
end
21+
end
22+
23+
instance_eval(&blk) if blk
24+
end
25+
end
26+
27+
def belongs_to(association_name, scope:, resource:, foreign_key:, primary_key: :id, &blk)
28+
_scope = scope
29+
30+
allow_sideload association_name, resource: resource do
31+
scope do |parents|
32+
parent_ids = parents.map { |p| p.send(foreign_key) }
33+
_scope.call.where(primary_key => parent_ids)
34+
end
35+
36+
assign do |parents, children|
37+
parents.each do |parent|
38+
relevant_child = children.find { |c| parent.send(foreign_key) == c.send(primary_key) }
39+
parent.send(:"#{association_name}=", relevant_child)
40+
end
41+
end
42+
end
43+
44+
instance_eval(&blk) if blk
45+
end
46+
47+
def has_one(association_name, scope:, resource:, foreign_key:, primary_key: :id, &blk)
48+
_scope = scope
49+
50+
allow_sideload association_name, resource: resource do
51+
scope do |parents|
52+
parent_ids = parents.map { |p| p.send(primary_key) }
53+
_scope.call.where(foreign_key => parent_ids)
54+
end
55+
56+
assign do |parents, children|
57+
parents.each do |parent|
58+
parent.association(association_name).loaded!
59+
relevant_child = children.find { |c| c.send(foreign_key) == parent.send(primary_key) }
60+
next unless relevant_child
61+
parent.send(:"#{association_name}=", relevant_child)
62+
end
63+
end
64+
65+
instance_eval(&blk) if blk
66+
end
67+
end
68+
end
69+
end
70+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module JsonapiCompliable
2+
module Adapters
3+
class Null < Abstract
4+
def filter(scope, attribute, value)
5+
scope
6+
end
7+
8+
def order(scope, attribute, direction)
9+
scope
10+
end
11+
12+
def paginate(scope, number, size)
13+
scope
14+
end
15+
16+
def sideload(scope, includes)
17+
scope
18+
end
19+
end
20+
end
21+
end

lib/jsonapi_compliable/base.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def render_jsonapi(scope, opts = {})
5050
options = default_jsonapi_render_options
5151
options[:include] = forced_includes || Util::IncludeParams.scrub(query_hash[:include], resource.allowed_sideloads)
5252
options[:jsonapi] = resolved
53-
options[:fields] = query.fieldsets
53+
options[:fields] = query_hash[:fields]
5454
options[:meta] ||= {}
5555
options.merge!(opts)
5656

@@ -107,7 +107,7 @@ def jsonapi(resource: nil, &blk)
107107
self._jsonapi_compliable = resource
108108
else
109109
if !self._jsonapi_compliable
110-
self._jsonapi_compliable = JsonapiCompliable::Resource
110+
self._jsonapi_compliable = Class.new(JsonapiCompliable::Resource)
111111
end
112112
end
113113

lib/jsonapi_compliable/extensions/extra_attribute.rb

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

17-
@extra_fields.include?(name)
17+
@extra_fields[@_type] && @extra_fields[@_type].include?(name)
1818
}
1919

2020
attribute name, if: allow_field, &blk

lib/jsonapi_compliable/query.rb

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ def self.default_hash
1010
sort: [],
1111
page: {},
1212
include: {},
13-
extra_fields: [],
14-
fields: [],
15-
stats: {}
13+
stats: {},
14+
fields: {},
15+
extra_fields: {}
1616
}
1717
end
1818

@@ -27,8 +27,13 @@ def to_hash
2727
hash[name] = self.class.default_hash.except(:include)
2828
end
2929

30-
parse_fields(hash, :fields)
31-
parse_fields(hash, :extra_fields)
30+
fields = parse_fields({}, :fields)
31+
extra_fields = parse_fields({}, :extra_fields)
32+
hash.each_pair do |type, query_hash|
33+
hash[type][:fields] = fields
34+
hash[type][:extra_fields] = extra_fields
35+
end
36+
3237
parse_filter(hash)
3338
parse_sort(hash)
3439
parse_pagination(hash)
@@ -38,17 +43,6 @@ def to_hash
3843
hash
3944
end
4045

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-
5246
def zero_results?
5347
!@params[:page].nil? &&
5448
!@params[:page][:size].nil? &&
@@ -61,7 +55,6 @@ def association?(name)
6155
resource.association_names.include?(name)
6256
end
6357

64-
# TODO: maybe walk the graph and apply to all
6558
def parse_include(hash)
6659
hash[resource.type][:include] = JSONAPI::IncludeDirective.new(params[:include] || {}).to_hash
6760
end
@@ -82,9 +75,7 @@ def parse_stats(hash)
8275

8376
def parse_fields(hash, type)
8477
field_params = Util::FieldParams.parse(params[type])
85-
field_params.each_pair do |namespace, fields|
86-
hash[namespace][type] = fields
87-
end
78+
hash[type] = field_params
8879
end
8980

9081
def parse_filter(hash)

lib/jsonapi_compliable/resource.rb

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ class Resource
77
:default_page_number,
88
:sorting,
99
:stats,
10-
:sideloads,
10+
:sideload_whitelist,
1111
:pagination,
1212
:extra_fields,
13+
:sideloading,
1314
:adapter,
1415
:type,
1516
:context
@@ -18,17 +19,27 @@ class << self
1819
attr_accessor :config
1920
end
2021

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+
2133
def self.inherited(klass)
2234
klass.config = self.config.deep_dup
2335
end
2436

25-
def self.includes(whitelist: nil, &blk)
26-
whitelist = JSONAPI::IncludeDirective.new(whitelist) if whitelist
37+
def self.sideloading
38+
config[:sideloading] ||= Sideload.new(:base, resource: self)
39+
end
2740

28-
config[:sideloads] = {
29-
whitelist: whitelist,
30-
custom_scope: blk
31-
}
41+
def self.sideload_whitelist(whitelist)
42+
config[:sideload_whitelist] = JSONAPI::IncludeDirective.new(whitelist).to_hash
3243
end
3344

3445
def self.allow_filter(name, *args, &blk)
@@ -89,6 +100,7 @@ def self.config
89100
@config ||= begin
90101
{
91102
sideloads: {},
103+
sideload_whitelist: {},
92104
filters: {},
93105
default_filters: {},
94106
extra_fields: {},
@@ -136,22 +148,22 @@ def build_scope(base, query, opts = {})
136148

137149
def association_names
138150
@association_names ||= begin
139-
if whitelist = sideloads[:whitelist]
140-
Util::Hash.keys(whitelist.to_hash.values.reduce(&:merge))
151+
if sideloading
152+
Util::Hash.keys(sideloading.to_hash[:base])
141153
else
142154
[]
143155
end
144156
end
145157
end
146158

147159
def allowed_sideloads
148-
return {} if sideloads.empty?
160+
return {} unless sideloading
149161

150-
if namespace = context[:namespace]
151-
sideloads[:whitelist][namespace].to_hash
152-
else
153-
sideloads[:whitelist].to_hash.values.reduce(&:merge)
162+
sideloads = sideloading.to_hash[:base]
163+
if !sideload_whitelist.empty? && context[:namespace]
164+
sideloads = Util::IncludeParams.scrub(sideloads, sideload_whitelist[context[:namespace]])
154165
end
166+
sideloads
155167
end
156168

157169
def stat(attribute, calculation)

lib/jsonapi_compliable/scope.rb

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
module JsonapiCompliable
22
class Scope
33
def initialize(object, resource, query, opts = {})
4-
@object = object
5-
@resource = resource
6-
@query = query
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
713

814
apply_scoping(opts)
915
end
@@ -16,21 +22,36 @@ def resolve
1622
if @query.zero_results?
1723
[]
1824
else
19-
@object
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
2030
end
2131
end
2232

2333
def query_hash
24-
@query_hash ||= @query.to_hash[@resource.type]
34+
@query_hash ||= @query.to_hash[@namespace]
2535
end
2636

2737
private
2838

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_scope = sideload.scope_proc.call(results)
44+
sideload_scope = Scope.new(sideload_scope, sideload.resource, @query, namespace: sideload.name)
45+
sideload_results = sideload_scope.resolve
46+
sideload.assign_proc.call(results, sideload_results)
47+
end
48+
end
49+
end
50+
2951
def apply_scoping(opts)
3052
@object = JsonapiCompliable::Scoping::DefaultFilter.new(@resource, query_hash, @object).apply
3153
@object = JsonapiCompliable::Scoping::Filter.new(@resource, query_hash, @object).apply unless opts[:filter] == false
3254
@object = JsonapiCompliable::Scoping::ExtraFields.new(@resource, query_hash, @object).apply unless opts[:extra_fields] == false
33-
@object = JsonapiCompliable::Scoping::Sideload.new(@resource, query_hash, @object).apply unless opts[:includes] == false
3455
@object = JsonapiCompliable::Scoping::Sort.new(@resource, query_hash, @object).apply unless opts[:sort] == false
3556
@unpaginated_object = @object
3657
@object = JsonapiCompliable::Scoping::Paginate.new(@resource, query_hash, @object).apply unless opts[:paginate] == false

0 commit comments

Comments
 (0)