jsonapi-resources 0.9.0 → 0.10.6
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 +5 -5
- data/LICENSE.txt +1 -1
- data/README.md +34 -11
- data/lib/bug_report_templates/rails_5_latest.rb +125 -0
- data/lib/bug_report_templates/rails_5_master.rb +140 -0
- data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +27 -0
- data/lib/jsonapi/active_relation/join_manager.rb +303 -0
- data/lib/jsonapi/active_relation_resource.rb +884 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +122 -105
- data/lib/jsonapi/basic_resource.rb +1162 -0
- data/lib/jsonapi/cached_response_fragment.rb +127 -0
- data/lib/jsonapi/compiled_json.rb +11 -1
- data/lib/jsonapi/configuration.rb +71 -8
- data/lib/jsonapi/error.rb +27 -0
- data/lib/jsonapi/error_codes.rb +2 -0
- data/lib/jsonapi/exceptions.rb +80 -50
- data/lib/jsonapi/formatter.rb +3 -3
- data/lib/jsonapi/include_directives.rb +18 -65
- data/lib/jsonapi/link_builder.rb +74 -80
- data/lib/jsonapi/operation.rb +16 -5
- data/lib/jsonapi/operation_result.rb +74 -16
- data/lib/jsonapi/path.rb +43 -0
- data/lib/jsonapi/path_segment.rb +76 -0
- data/lib/jsonapi/processor.rb +239 -111
- data/lib/jsonapi/relationship.rb +153 -15
- data/lib/jsonapi/request_parser.rb +430 -367
- data/lib/jsonapi/resource.rb +3 -1253
- data/lib/jsonapi/resource_controller_metal.rb +5 -2
- data/lib/jsonapi/resource_fragment.rb +47 -0
- data/lib/jsonapi/resource_id_tree.rb +112 -0
- data/lib/jsonapi/resource_identity.rb +42 -0
- data/lib/jsonapi/resource_serializer.rb +143 -285
- data/lib/jsonapi/resource_set.rb +176 -0
- data/lib/jsonapi/resources/railtie.rb +9 -0
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +105 -83
- data/lib/jsonapi/routing_ext.rb +48 -26
- data/lib/jsonapi-resources.rb +20 -4
- data/lib/tasks/check_upgrade.rake +52 -0
- metadata +50 -20
- data/lib/jsonapi/cached_resource_fragment.rb +0 -127
- data/lib/jsonapi/operation_dispatcher.rb +0 -88
- data/lib/jsonapi/operation_results.rb +0 -35
- data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -4,14 +4,12 @@ module JSONAPI
|
|
4
4
|
# For example ['posts.comments.tags']
|
5
5
|
# will transform into =>
|
6
6
|
# {
|
7
|
-
# posts:{
|
8
|
-
#
|
9
|
-
# include_related:{
|
7
|
+
# posts: {
|
8
|
+
# include_related: {
|
10
9
|
# comments:{
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# include:true
|
10
|
+
# include_related: {
|
11
|
+
# tags: {
|
12
|
+
# include_related: {}
|
15
13
|
# }
|
16
14
|
# }
|
17
15
|
# }
|
@@ -19,9 +17,8 @@ module JSONAPI
|
|
19
17
|
# }
|
20
18
|
# }
|
21
19
|
|
22
|
-
def initialize(resource_klass, includes_array
|
20
|
+
def initialize(resource_klass, includes_array)
|
23
21
|
@resource_klass = resource_klass
|
24
|
-
@force_eager_load = force_eager_load
|
25
22
|
@include_directives_hash = { include_related: {} }
|
26
23
|
includes_array.each do |include|
|
27
24
|
parse_include(include)
|
@@ -32,69 +29,25 @@ module JSONAPI
|
|
32
29
|
@include_directives_hash
|
33
30
|
end
|
34
31
|
|
35
|
-
def model_includes
|
36
|
-
get_includes(@include_directives_hash)
|
37
|
-
end
|
38
|
-
|
39
|
-
def paths
|
40
|
-
delve_paths(get_includes(@include_directives_hash, false))
|
41
|
-
end
|
42
|
-
|
43
32
|
private
|
44
33
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
if current_resource_klass
|
52
|
-
current_relationship = current_resource_klass._relationships[fragment]
|
53
|
-
current_resource_klass = current_relationship.try(:resource_klass)
|
54
|
-
else
|
55
|
-
warn "[RELATIONSHIP NOT FOUND] Relationship could not be found for #{current_path}."
|
56
|
-
end
|
57
|
-
|
58
|
-
include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include
|
59
|
-
|
60
|
-
current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
|
61
|
-
current = current[:include_related][fragment]
|
62
|
-
end
|
63
|
-
current
|
64
|
-
end
|
65
|
-
|
66
|
-
def get_includes(directive, only_joined_includes = true)
|
67
|
-
ir = directive[:include_related]
|
68
|
-
ir = ir.select { |k,v| v[:include_in_join] } if only_joined_includes
|
34
|
+
def parse_include(include)
|
35
|
+
path = JSONAPI::Path.new(resource_klass: @resource_klass,
|
36
|
+
path_string: include,
|
37
|
+
ensure_default_field: false,
|
38
|
+
parse_fields: false)
|
69
39
|
|
70
|
-
|
71
|
-
sub = get_includes(sub_directive, only_joined_includes)
|
72
|
-
sub.any? ? { name => sub } : name
|
73
|
-
end
|
74
|
-
end
|
40
|
+
current = @include_directives_hash
|
75
41
|
|
76
|
-
|
77
|
-
|
78
|
-
local_path = ''
|
42
|
+
path.segments.each do |segment|
|
43
|
+
relationship_name = segment.relationship.name.to_sym
|
79
44
|
|
80
|
-
|
81
|
-
|
82
|
-
related = get_related(local_path)
|
83
|
-
related[:include] = true
|
45
|
+
current[:include_related][relationship_name] ||= { include_related: {} }
|
46
|
+
current = current[:include_related][relationship_name]
|
84
47
|
end
|
85
|
-
end
|
86
48
|
|
87
|
-
|
88
|
-
|
89
|
-
when Array
|
90
|
-
obj.map{|elem| delve_paths(elem)}.flatten(1)
|
91
|
-
when Hash
|
92
|
-
obj.map{|k,v| [[k]] + delve_paths(v).map{|path| [k] + path } }.flatten(1)
|
93
|
-
when Symbol, String
|
94
|
-
[[obj]]
|
95
|
-
else
|
96
|
-
raise "delve_paths cannot descend into #{obj.class.name}"
|
97
|
-
end
|
49
|
+
rescue JSONAPI::Exceptions::InvalidRelationship => _e
|
50
|
+
raise JSONAPI::Exceptions::InvalidInclude.new(@resource_klass, include)
|
98
51
|
end
|
99
52
|
end
|
100
53
|
end
|
data/lib/jsonapi/link_builder.rb
CHANGED
@@ -3,107 +3,100 @@ module JSONAPI
|
|
3
3
|
attr_reader :base_url,
|
4
4
|
:primary_resource_klass,
|
5
5
|
:route_formatter,
|
6
|
-
:
|
6
|
+
:engine,
|
7
|
+
:engine_mount_point,
|
8
|
+
:url_helpers
|
9
|
+
|
10
|
+
@@url_helper_methods = {}
|
7
11
|
|
8
12
|
def initialize(config = {})
|
9
|
-
@base_url
|
13
|
+
@base_url = config[:base_url]
|
10
14
|
@primary_resource_klass = config[:primary_resource_klass]
|
11
|
-
@route_formatter
|
12
|
-
@
|
15
|
+
@route_formatter = config[:route_formatter]
|
16
|
+
@engine = build_engine
|
17
|
+
@engine_mount_point = @engine ? @engine.routes.find_script_name({}) : ""
|
13
18
|
|
14
|
-
#
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
end
|
19
|
+
# url_helpers may be either a controller which has the route helper methods, or the application router's
|
20
|
+
# url helpers module, `Rails.application.routes.url_helpers`. Because the method no longer behaves as a
|
21
|
+
# singleton, and it's expensive to generate the module, the controller is preferred.
|
22
|
+
@url_helpers = config[:url_helpers]
|
19
23
|
end
|
20
24
|
|
21
25
|
def engine?
|
22
|
-
!!@
|
26
|
+
!!@engine
|
23
27
|
end
|
24
28
|
|
25
29
|
def primary_resources_url
|
26
|
-
if
|
27
|
-
|
30
|
+
if @primary_resource_klass._routed
|
31
|
+
primary_resources_path = resources_path(primary_resource_klass)
|
32
|
+
@primary_resources_url_cached ||= "#{ base_url }#{ engine_mount_point }#{ primary_resources_path }"
|
28
33
|
else
|
29
|
-
|
34
|
+
if JSONAPI.configuration.warn_on_missing_routes && !@primary_resource_klass._warned_missing_route
|
35
|
+
warn "primary_resources_url for #{@primary_resource_klass} could not be generated"
|
36
|
+
@primary_resource_klass._warned_missing_route = true
|
37
|
+
end
|
38
|
+
nil
|
30
39
|
end
|
31
40
|
end
|
32
41
|
|
33
42
|
def query_link(query_params)
|
34
|
-
|
43
|
+
url = primary_resources_url
|
44
|
+
return url if url.nil?
|
45
|
+
"#{ url }?#{ query_params.to_query }"
|
35
46
|
end
|
36
47
|
|
37
48
|
def relationships_related_link(source, relationship, query_params = {})
|
38
|
-
|
39
|
-
|
40
|
-
|
49
|
+
if relationship._routed
|
50
|
+
url = "#{ self_link(source) }/#{ route_for_relationship(relationship) }"
|
51
|
+
url = "#{ url }?#{ query_params.to_query }" if query_params.present?
|
52
|
+
url
|
53
|
+
else
|
54
|
+
if JSONAPI.configuration.warn_on_missing_routes && !relationship._warned_missing_route
|
55
|
+
warn "related_link for #{relationship} could not be generated"
|
56
|
+
relationship._warned_missing_route = true
|
57
|
+
end
|
58
|
+
nil
|
59
|
+
end
|
41
60
|
end
|
42
61
|
|
43
62
|
def relationships_self_link(source, relationship)
|
44
|
-
|
63
|
+
if relationship._routed
|
64
|
+
"#{ self_link(source) }/relationships/#{ route_for_relationship(relationship) }"
|
65
|
+
else
|
66
|
+
if JSONAPI.configuration.warn_on_missing_routes && !relationship._warned_missing_route
|
67
|
+
warn "self_link for #{relationship} could not be generated"
|
68
|
+
relationship._warned_missing_route = true
|
69
|
+
end
|
70
|
+
nil
|
71
|
+
end
|
45
72
|
end
|
46
73
|
|
47
74
|
def self_link(source)
|
48
|
-
if
|
49
|
-
|
75
|
+
if source.class._routed
|
76
|
+
resource_url(source)
|
50
77
|
else
|
51
|
-
|
78
|
+
if JSONAPI.configuration.warn_on_missing_routes && !source.class._warned_missing_route
|
79
|
+
warn "self_link for #{source.class} could not be generated"
|
80
|
+
source.class._warned_missing_route = true
|
81
|
+
end
|
82
|
+
nil
|
52
83
|
end
|
53
84
|
end
|
54
85
|
|
55
86
|
private
|
56
87
|
|
57
|
-
def
|
88
|
+
def build_engine
|
58
89
|
scopes = module_scopes_from_class(primary_resource_klass)
|
59
90
|
|
60
91
|
begin
|
61
92
|
unless scopes.empty?
|
62
93
|
"#{ scopes.first.to_s.camelize }::Engine".safe_constantize
|
63
94
|
end
|
64
|
-
rescue LoadError => e
|
65
|
-
nil
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def engine_path_from_resource_class(klass)
|
70
|
-
path_name = engine_resources_path_name_from_class(klass)
|
71
|
-
engine_name.routes.url_helpers.public_send(path_name)
|
72
|
-
end
|
73
95
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
def engine_primary_resources_url
|
79
|
-
"#{ base_url }#{ engine_primary_resources_path }"
|
80
|
-
end
|
81
|
-
|
82
|
-
def engine_resource_path(source)
|
83
|
-
resource_path_name = engine_resource_path_name_from_source(source)
|
84
|
-
engine_name.routes.url_helpers.public_send(resource_path_name, source.id)
|
85
|
-
end
|
86
|
-
|
87
|
-
def engine_resource_path_name_from_source(source)
|
88
|
-
scopes = module_scopes_from_class(source.class)[1..-1]
|
89
|
-
base_path_name = scopes.map { |scope| scope.underscore }.join("_")
|
90
|
-
end_path_name = source.class._type.to_s.singularize
|
91
|
-
[base_path_name, end_path_name, "path"].reject(&:blank?).join("_")
|
92
|
-
end
|
93
|
-
|
94
|
-
def engine_resource_url(source)
|
95
|
-
"#{ base_url }#{ engine_resource_path(source) }"
|
96
|
-
end
|
97
|
-
|
98
|
-
def engine_resources_path_name_from_class(klass)
|
99
|
-
scopes = module_scopes_from_class(klass)[1..-1]
|
100
|
-
base_path_name = scopes.map { |scope| scope.underscore }.join("_")
|
101
|
-
end_path_name = klass._type.to_s
|
102
|
-
|
103
|
-
if base_path_name.blank?
|
104
|
-
"#{ end_path_name }_path"
|
105
|
-
else
|
106
|
-
"#{ base_path_name }_#{ end_path_name }_path"
|
96
|
+
# :nocov:
|
97
|
+
rescue LoadError => _e
|
98
|
+
nil
|
99
|
+
# :nocov:
|
107
100
|
end
|
108
101
|
end
|
109
102
|
|
@@ -112,10 +105,14 @@ module JSONAPI
|
|
112
105
|
end
|
113
106
|
|
114
107
|
def formatted_module_path_from_class(klass)
|
115
|
-
scopes =
|
108
|
+
scopes = if @engine
|
109
|
+
module_scopes_from_class(klass)[1..-1]
|
110
|
+
else
|
111
|
+
module_scopes_from_class(klass)
|
112
|
+
end
|
116
113
|
|
117
114
|
unless scopes.empty?
|
118
|
-
"/#{ scopes.map{
|
115
|
+
"/#{ scopes.map {|scope| format_route(scope.to_s.underscore)}.compact.join('/') }/"
|
119
116
|
else
|
120
117
|
"/"
|
121
118
|
end
|
@@ -125,24 +122,21 @@ module JSONAPI
|
|
125
122
|
klass.name.to_s.split("::")[0...-1]
|
126
123
|
end
|
127
124
|
|
128
|
-
def
|
129
|
-
@
|
130
|
-
|
131
|
-
|
132
|
-
def regular_primary_resources_path
|
133
|
-
regular_resources_path(primary_resource_klass)
|
125
|
+
def resources_path(source_klass)
|
126
|
+
@_resources_path ||= {}
|
127
|
+
@_resources_path[source_klass] ||= formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
|
134
128
|
end
|
135
129
|
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
130
|
+
def resource_path(source)
|
131
|
+
if source.class.singleton?
|
132
|
+
resources_path(source.class)
|
133
|
+
else
|
134
|
+
"#{resources_path(source.class)}/#{source.id}"
|
135
|
+
end
|
142
136
|
end
|
143
137
|
|
144
|
-
def
|
145
|
-
"#{ base_url }#{
|
138
|
+
def resource_url(source)
|
139
|
+
"#{ base_url }#{ engine_mount_point }#{ resource_path(source) }"
|
146
140
|
end
|
147
141
|
|
148
142
|
def route_for_relationship(relationship)
|
data/lib/jsonapi/operation.rb
CHANGED
@@ -8,17 +8,28 @@ module JSONAPI
|
|
8
8
|
@options = options
|
9
9
|
end
|
10
10
|
|
11
|
-
def transactional?
|
12
|
-
JSONAPI::Processor._processor_from_resource_type(resource_klass).transactional_operation_type?(operation_type)
|
13
|
-
end
|
14
|
-
|
15
11
|
def process
|
16
12
|
processor.process
|
17
13
|
end
|
18
14
|
|
19
15
|
private
|
20
16
|
def processor
|
21
|
-
|
17
|
+
self.class.processor_instance_for(resource_klass, operation_type, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def processor_instance_for(resource_klass, operation_type, params)
|
22
|
+
_processor_from_resource_type(resource_klass).new(resource_klass, operation_type, params)
|
23
|
+
end
|
24
|
+
|
25
|
+
def _processor_from_resource_type(resource_klass)
|
26
|
+
processor = resource_klass.name.gsub(/Resource$/,'Processor').safe_constantize
|
27
|
+
if processor.nil?
|
28
|
+
processor = JSONAPI.configuration.default_processor_klass
|
29
|
+
end
|
30
|
+
|
31
|
+
return processor
|
32
|
+
end
|
22
33
|
end
|
23
34
|
end
|
24
35
|
end
|
@@ -4,12 +4,18 @@ module JSONAPI
|
|
4
4
|
attr_accessor :meta
|
5
5
|
attr_accessor :links
|
6
6
|
attr_accessor :options
|
7
|
+
attr_accessor :warnings
|
7
8
|
|
8
9
|
def initialize(code, options = {})
|
9
|
-
@code = code
|
10
|
+
@code = Rack::Utils.status_code(code)
|
10
11
|
@options = options
|
11
12
|
@meta = options.fetch(:meta, {})
|
12
13
|
@links = options.fetch(:links, {})
|
14
|
+
@warnings = options.fetch(:warnings, {})
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash(serializer = nil)
|
18
|
+
{}
|
13
19
|
end
|
14
20
|
end
|
15
21
|
|
@@ -20,46 +26,98 @@ module JSONAPI
|
|
20
26
|
@errors = errors
|
21
27
|
super(code, options)
|
22
28
|
end
|
29
|
+
|
30
|
+
def to_hash(serializer = nil)
|
31
|
+
{
|
32
|
+
errors: errors.collect do |error|
|
33
|
+
# :nocov:
|
34
|
+
error.to_hash
|
35
|
+
# :nocov:
|
36
|
+
end
|
37
|
+
}
|
38
|
+
end
|
23
39
|
end
|
24
40
|
|
25
|
-
class
|
26
|
-
attr_accessor :
|
41
|
+
class ResourceSetOperationResult < OperationResult
|
42
|
+
attr_accessor :resource_set, :pagination_params
|
27
43
|
|
28
|
-
def initialize(code,
|
29
|
-
@
|
44
|
+
def initialize(code, resource_set, options = {})
|
45
|
+
@resource_set = resource_set
|
46
|
+
@pagination_params = options.fetch(:pagination_params, {})
|
30
47
|
super(code, options)
|
31
48
|
end
|
49
|
+
|
50
|
+
def to_hash(serializer)
|
51
|
+
if serializer
|
52
|
+
serializer.serialize_resource_set_to_hash_single(resource_set)
|
53
|
+
else
|
54
|
+
# :nocov:
|
55
|
+
{}
|
56
|
+
# :nocov:
|
57
|
+
end
|
58
|
+
end
|
32
59
|
end
|
33
60
|
|
34
|
-
class
|
35
|
-
attr_accessor :
|
61
|
+
class ResourcesSetOperationResult < OperationResult
|
62
|
+
attr_accessor :resource_set, :pagination_params, :record_count, :page_count
|
36
63
|
|
37
|
-
def initialize(code,
|
38
|
-
@
|
64
|
+
def initialize(code, resource_set, options = {})
|
65
|
+
@resource_set = resource_set
|
39
66
|
@pagination_params = options.fetch(:pagination_params, {})
|
40
67
|
@record_count = options[:record_count]
|
41
68
|
@page_count = options[:page_count]
|
42
69
|
super(code, options)
|
43
70
|
end
|
71
|
+
|
72
|
+
def to_hash(serializer)
|
73
|
+
if serializer
|
74
|
+
serializer.serialize_resource_set_to_hash_plural(resource_set)
|
75
|
+
else
|
76
|
+
# :nocov:
|
77
|
+
{}
|
78
|
+
# :nocov:
|
79
|
+
end
|
80
|
+
end
|
44
81
|
end
|
45
82
|
|
46
|
-
class
|
47
|
-
attr_accessor :source_resource, :_type
|
83
|
+
class RelatedResourcesSetOperationResult < ResourcesSetOperationResult
|
84
|
+
attr_accessor :resource_set, :source_resource, :_type
|
48
85
|
|
49
|
-
def initialize(code, source_resource, type,
|
86
|
+
def initialize(code, source_resource, type, resource_set, options = {})
|
50
87
|
@source_resource = source_resource
|
51
88
|
@_type = type
|
52
|
-
super(code,
|
89
|
+
super(code, resource_set, options)
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_hash(serializer = nil)
|
93
|
+
if serializer
|
94
|
+
serializer.serialize_related_resource_set_to_hash_plural(resource_set, source_resource)
|
95
|
+
else
|
96
|
+
# :nocov:
|
97
|
+
{}
|
98
|
+
# :nocov:
|
99
|
+
end
|
53
100
|
end
|
54
101
|
end
|
55
102
|
|
56
|
-
class
|
57
|
-
attr_accessor :parent_resource, :relationship
|
103
|
+
class RelationshipOperationResult < OperationResult
|
104
|
+
attr_accessor :parent_resource, :relationship, :resource_ids
|
58
105
|
|
59
|
-
def initialize(code, parent_resource, relationship, options = {})
|
106
|
+
def initialize(code, parent_resource, relationship, resource_ids, options = {})
|
60
107
|
@parent_resource = parent_resource
|
61
108
|
@relationship = relationship
|
109
|
+
@resource_ids = resource_ids
|
62
110
|
super(code, options)
|
63
111
|
end
|
112
|
+
|
113
|
+
def to_hash(serializer = nil)
|
114
|
+
if serializer
|
115
|
+
serializer.serialize_to_relationship_hash(parent_resource, relationship, resource_ids)
|
116
|
+
else
|
117
|
+
# :nocov:
|
118
|
+
{}
|
119
|
+
# :nocov:
|
120
|
+
end
|
121
|
+
end
|
64
122
|
end
|
65
123
|
end
|
data/lib/jsonapi/path.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class Path
|
3
|
+
attr_reader :segments, :resource_klass
|
4
|
+
def initialize(resource_klass:,
|
5
|
+
path_string:,
|
6
|
+
ensure_default_field: true,
|
7
|
+
parse_fields: true)
|
8
|
+
@resource_klass = resource_klass
|
9
|
+
|
10
|
+
current_resource_klass = resource_klass
|
11
|
+
@segments = path_string.to_s.split('.').collect do |segment_string|
|
12
|
+
segment = PathSegment.parse(source_resource_klass: current_resource_klass,
|
13
|
+
segment_string: segment_string,
|
14
|
+
parse_fields: parse_fields)
|
15
|
+
|
16
|
+
current_resource_klass = segment.resource_klass
|
17
|
+
segment
|
18
|
+
end
|
19
|
+
|
20
|
+
if ensure_default_field && parse_fields && @segments.last.is_a?(PathSegment::Relationship)
|
21
|
+
last = @segments.last
|
22
|
+
@segments << PathSegment::Field.new(resource_klass: last.resource_klass,
|
23
|
+
field_name: last.resource_klass._primary_key)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def relationship_segments
|
28
|
+
@segments.select {|p| p.is_a?(PathSegment::Relationship)}
|
29
|
+
end
|
30
|
+
|
31
|
+
def relationship_path_string
|
32
|
+
relationship_segments.collect(&:to_s).join('.')
|
33
|
+
end
|
34
|
+
|
35
|
+
def last_relationship
|
36
|
+
if @segments.last.is_a?(PathSegment::Relationship)
|
37
|
+
@segments.last
|
38
|
+
else
|
39
|
+
@segments[-2]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class PathSegment
|
3
|
+
def self.parse(source_resource_klass:, segment_string:, parse_fields: true)
|
4
|
+
first_part, last_part = segment_string.split('#', 2)
|
5
|
+
relationship = source_resource_klass._relationship(first_part)
|
6
|
+
|
7
|
+
if relationship
|
8
|
+
if last_part
|
9
|
+
unless relationship.resource_types.include?(last_part)
|
10
|
+
raise JSONAPI::Exceptions::InvalidRelationship.new(source_resource_klass._type, segment_string)
|
11
|
+
end
|
12
|
+
resource_klass = source_resource_klass.resource_klass_for(last_part)
|
13
|
+
end
|
14
|
+
return PathSegment::Relationship.new(relationship: relationship, resource_klass: resource_klass)
|
15
|
+
else
|
16
|
+
if last_part.blank? && parse_fields
|
17
|
+
return PathSegment::Field.new(resource_klass: source_resource_klass, field_name: first_part)
|
18
|
+
else
|
19
|
+
raise JSONAPI::Exceptions::InvalidRelationship.new(source_resource_klass._type, segment_string)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Relationship
|
25
|
+
attr_reader :relationship, :resource_klass
|
26
|
+
|
27
|
+
def initialize(relationship:, resource_klass: nil)
|
28
|
+
@relationship = relationship
|
29
|
+
@resource_klass = resource_klass
|
30
|
+
end
|
31
|
+
|
32
|
+
def eql?(other)
|
33
|
+
other.is_a?(JSONAPI::PathSegment::Relationship) && relationship == other.relationship && resource_klass == other.resource_klass
|
34
|
+
end
|
35
|
+
|
36
|
+
def hash
|
37
|
+
[relationship, resource_klass].hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@resource_klass ? "#{relationship.parent_resource_klass._type}.#{relationship.name}##{resource_klass._type}" : "#{resource_klass._type}.#{relationship.name}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def resource_klass
|
45
|
+
@resource_klass || relationship.resource_klass
|
46
|
+
end
|
47
|
+
|
48
|
+
def path_specified_resource_klass?
|
49
|
+
!@resource_klass.nil?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Field
|
54
|
+
attr_reader :resource_klass, :field_name
|
55
|
+
|
56
|
+
def initialize(resource_klass:, field_name:)
|
57
|
+
@resource_klass = resource_klass
|
58
|
+
@field_name = field_name
|
59
|
+
end
|
60
|
+
|
61
|
+
def eql?(other)
|
62
|
+
other.is_a?(JSONAPI::PathSegment::Field) && field_name == other.field_name && resource_klass == other.resource_klass
|
63
|
+
end
|
64
|
+
|
65
|
+
def delegated_field_name
|
66
|
+
resource_klass._attribute_delegated_name(field_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s
|
70
|
+
# :nocov:
|
71
|
+
"#{resource_klass._type}.#{field_name.to_s}"
|
72
|
+
# :nocov:
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|