jsonapi-resources 0.10.0.beta3 → 0.10.0.beta4

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.
@@ -1,6 +1,5 @@
1
1
  require 'jsonapi/formatter'
2
2
  require 'jsonapi/processor'
3
- require 'jsonapi/active_relation_resource_finder'
4
3
  require 'concurrent'
5
4
 
6
5
  module JSONAPI
@@ -10,6 +9,7 @@ module JSONAPI
10
9
  :route_format,
11
10
  :raise_if_parameters_not_allowed,
12
11
  :warn_on_route_setup_issues,
12
+ :warn_on_missing_routes,
13
13
  :default_allow_include_to_one,
14
14
  :default_allow_include_to_many,
15
15
  :allow_sort,
@@ -17,7 +17,6 @@ module JSONAPI
17
17
  :default_paginator,
18
18
  :default_page_size,
19
19
  :maximum_page_size,
20
- :resource_finder,
21
20
  :default_processor_klass,
22
21
  :use_text_errors,
23
22
  :top_level_links_include_pagination,
@@ -59,6 +58,7 @@ module JSONAPI
59
58
  self.raise_if_parameters_not_allowed = true
60
59
 
61
60
  self.warn_on_route_setup_issues = true
61
+ self.warn_on_missing_routes = true
62
62
 
63
63
  # :none, :offset, :paged, or a custom paginator name
64
64
  self.default_paginator = :none
@@ -105,12 +105,6 @@ module JSONAPI
105
105
  self.always_include_to_one_linkage_data = false
106
106
  self.always_include_to_many_linkage_data = false
107
107
 
108
- # ResourceFinder Mixin
109
- # The default ResourceFinder is the ActiveRelationResourceFinder which provides
110
- # access to ActiveRelation backed models. Custom ResourceFinders can be specified
111
- # in order to support other ORMs.
112
- self.resource_finder = JSONAPI::ActiveRelationResourceFinder
113
-
114
108
  # The default Operation Processor to use if one is not defined specifically
115
109
  # for a Resource.
116
110
  self.default_processor_klass = JSONAPI::Processor
@@ -225,10 +219,6 @@ module JSONAPI
225
219
  @default_processor_klass = default_processor_klass
226
220
  end
227
221
 
228
- def resource_finder=(resource_finder)
229
- @resource_finder = resource_finder
230
- end
231
-
232
222
  def allow_include=(allow_include)
233
223
  ActiveSupport::Deprecation.warn('`allow_include` has been replaced by `default_allow_include_to_one` and `default_allow_include_to_many` options.')
234
224
  @default_allow_include_to_one = allow_include
@@ -273,6 +263,8 @@ module JSONAPI
273
263
 
274
264
  attr_writer :warn_on_route_setup_issues
275
265
 
266
+ attr_writer :warn_on_missing_routes
267
+
276
268
  attr_writer :use_relationship_reflection
277
269
 
278
270
  attr_writer :resource_cache
@@ -4,14 +4,12 @@ module JSONAPI
4
4
  # For example ['posts.comments.tags']
5
5
  # will transform into =>
6
6
  # {
7
- # posts:{
8
- # include:true,
9
- # include_related:{
7
+ # posts: {
8
+ # include_related: {
10
9
  # comments:{
11
- # include:true,
12
- # include_related:{
13
- # tags:{
14
- # include:true
10
+ # include_related: {
11
+ # tags: {
12
+ # include_related: {}
15
13
  # }
16
14
  # }
17
15
  # }
@@ -44,7 +42,7 @@ module JSONAPI
44
42
  path.segments.each do |segment|
45
43
  relationship_name = segment.relationship.name.to_sym
46
44
 
47
- current[:include_related][relationship_name] ||= { include: true, include_related: {} }
45
+ current[:include_related][relationship_name] ||= { include_related: {} }
48
46
  current = current[:include_related][relationship_name]
49
47
  end
50
48
 
@@ -2,32 +2,33 @@ module JSONAPI
2
2
  class LinkBuilder
3
3
  attr_reader :base_url,
4
4
  :primary_resource_klass,
5
- :route_formatter,
6
- :engine_name
5
+ :engine,
6
+ :routes
7
7
 
8
8
  def initialize(config = {})
9
9
  @base_url = config[:base_url]
10
10
  @primary_resource_klass = config[:primary_resource_klass]
11
- @route_formatter = config[:route_formatter]
12
- @engine_name = build_engine_name
11
+ @engine = build_engine
13
12
 
14
- # Warning: These make LinkBuilder non-thread-safe. That's not a problem with the
15
- # request-specific way it's currently used, though.
16
- @resources_path_cache = JSONAPI::NaiveCache.new do |source_klass|
17
- formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
13
+ if engine?
14
+ @routes = @engine.routes
15
+ else
16
+ @routes = Rails.application.routes
18
17
  end
18
+
19
+ # ToDo: Use NaiveCache for values. For this we need to not return nils and create composite keys which work
20
+ # as efficient cache lookups. This could be an array of the [source.identifier, relationship] since the
21
+ # ResourceIdentity will compare equality correctly
19
22
  end
20
23
 
21
24
  def engine?
22
- !!@engine_name
25
+ !!@engine
23
26
  end
24
27
 
25
28
  def primary_resources_url
26
- if engine?
27
- engine_primary_resources_url
28
- else
29
- regular_primary_resources_url
30
- end
29
+ @primary_resources_url_cached ||= "#{ base_url }#{ primary_resources_path }"
30
+ rescue NoMethodError
31
+ warn "primary_resources_url for #{@primary_resource_klass} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
31
32
  end
32
33
 
33
34
  def query_link(query_params)
@@ -35,26 +36,45 @@ module JSONAPI
35
36
  end
36
37
 
37
38
  def relationships_related_link(source, relationship, query_params = {})
38
- url = "#{ self_link(source) }/#{ route_for_relationship(relationship) }"
39
+ if relationship.parent_resource.singleton?
40
+ url_helper_name = singleton_related_url_helper_name(relationship)
41
+ url = call_url_helper(url_helper_name)
42
+ else
43
+ url_helper_name = related_url_helper_name(relationship)
44
+ url = call_url_helper(url_helper_name, source.id)
45
+ end
46
+
47
+ url = "#{ base_url }#{ url }"
39
48
  url = "#{ url }?#{ query_params.to_query }" if query_params.present?
40
49
  url
50
+ rescue NoMethodError
51
+ warn "related_link for #{relationship} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
41
52
  end
42
53
 
43
54
  def relationships_self_link(source, relationship)
44
- "#{ self_link(source) }/relationships/#{ route_for_relationship(relationship) }"
55
+ if relationship.parent_resource.singleton?
56
+ url_helper_name = singleton_relationship_self_url_helper_name(relationship)
57
+ url = call_url_helper(url_helper_name)
58
+ else
59
+ url_helper_name = relationship_self_url_helper_name(relationship)
60
+ url = call_url_helper(url_helper_name, source.id)
61
+ end
62
+
63
+ url = "#{ base_url }#{ url }"
64
+ url
65
+ rescue NoMethodError
66
+ warn "self_link for #{relationship} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
45
67
  end
46
68
 
47
69
  def self_link(source)
48
- if engine?
49
- engine_resource_url(source)
50
- else
51
- regular_resource_url(source)
52
- end
70
+ "#{ base_url }#{ resource_path(source) }"
71
+ rescue NoMethodError
72
+ warn "self_link for #{source.class} could not be generated" if JSONAPI.configuration.warn_on_missing_routes
53
73
  end
54
74
 
55
75
  private
56
76
 
57
- def build_engine_name
77
+ def build_engine
58
78
  scopes = module_scopes_from_class(primary_resource_klass)
59
79
 
60
80
  begin
@@ -68,93 +88,96 @@ module JSONAPI
68
88
  end
69
89
  end
70
90
 
71
- def engine_path_from_resource_class(klass)
72
- path_name = engine_resources_path_name_from_class(klass)
73
- engine_name.routes.url_helpers.public_send(path_name)
91
+ def call_url_helper(method, *args)
92
+ routes.url_helpers.public_send(method, args)
93
+ rescue NoMethodError => e
94
+ raise e
74
95
  end
75
96
 
76
- def engine_primary_resources_path
77
- engine_path_from_resource_class(primary_resource_klass)
97
+ def path_from_resource_class(klass)
98
+ url_helper_name = resources_url_helper_name_from_class(klass)
99
+ call_url_helper(url_helper_name)
78
100
  end
79
101
 
80
- def engine_primary_resources_url
81
- "#{ base_url }#{ engine_primary_resources_path }"
102
+ def resource_path(source)
103
+ url_helper_name = resource_url_helper_name_from_source(source)
104
+ if source.class.singleton?
105
+ call_url_helper(url_helper_name)
106
+ else
107
+ call_url_helper(url_helper_name, source.id)
108
+ end
82
109
  end
83
110
 
84
- def engine_resource_path(source)
85
- resource_path_name = engine_resource_path_name_from_source(source)
86
- engine_name.routes.url_helpers.public_send(resource_path_name, source.id)
111
+ def primary_resources_path
112
+ path_from_resource_class(primary_resource_klass)
87
113
  end
88
114
 
89
- def engine_resource_path_name_from_source(source)
90
- scopes = module_scopes_from_class(source.class)[1..-1]
91
- base_path_name = scopes.map { |scope| scope.underscore }.join("_")
92
- end_path_name = source.class._type.to_s.singularize
93
- [base_path_name, end_path_name, "path"].reject(&:blank?).join("_")
115
+ def url_helper_name_from_parts(parts)
116
+ (parts << "path").reject(&:blank?).join("_")
94
117
  end
95
118
 
96
- def engine_resource_url(source)
97
- "#{ base_url }#{ engine_resource_path(source) }"
98
- end
119
+ def resources_path_parts_from_class(klass)
120
+ if engine?
121
+ scopes = module_scopes_from_class(klass)[1..-1]
122
+ else
123
+ scopes = module_scopes_from_class(klass)
124
+ end
99
125
 
100
- def engine_resources_path_name_from_class(klass)
101
- scopes = module_scopes_from_class(klass)[1..-1]
102
126
  base_path_name = scopes.map { |scope| scope.underscore }.join("_")
103
127
  end_path_name = klass._type.to_s
104
-
105
- if base_path_name.blank?
106
- "#{ end_path_name }_path"
107
- else
108
- "#{ base_path_name }_#{ end_path_name }_path"
109
- end
128
+ [base_path_name, end_path_name]
110
129
  end
111
130
 
112
- def format_route(route)
113
- route_formatter.format(route)
131
+ def resources_url_helper_name_from_class(klass)
132
+ url_helper_name_from_parts(resources_path_parts_from_class(klass))
114
133
  end
115
134
 
116
- def formatted_module_path_from_class(klass)
117
- scopes = module_scopes_from_class(klass)
118
-
119
- unless scopes.empty?
120
- "/#{ scopes.map{ |scope| format_route(scope.to_s.underscore) }.compact.join('/') }/"
135
+ def resource_path_parts_from_class(klass)
136
+ if engine?
137
+ scopes = module_scopes_from_class(klass)[1..-1]
121
138
  else
122
- "/"
139
+ scopes = module_scopes_from_class(klass)
123
140
  end
124
- end
125
141
 
126
- def module_scopes_from_class(klass)
127
- klass.name.to_s.split("::")[0...-1]
142
+ base_path_name = scopes.map { |scope| scope.underscore }.join("_")
143
+ end_path_name = klass._type.to_s.singularize
144
+ [base_path_name, end_path_name]
128
145
  end
129
146
 
130
- def regular_resources_path(source_klass)
131
- @resources_path_cache.get(source_klass)
147
+ def resource_url_helper_name_from_source(source)
148
+ url_helper_name_from_parts(resource_path_parts_from_class(source.class))
132
149
  end
133
150
 
134
- def regular_primary_resources_path
135
- regular_resources_path(primary_resource_klass)
151
+ def related_url_helper_name(relationship)
152
+ relationship_parts = resource_path_parts_from_class(relationship.parent_resource)
153
+ relationship_parts << relationship.name
154
+ url_helper_name_from_parts(relationship_parts)
136
155
  end
137
156
 
138
- def regular_primary_resources_url
139
- "#{ base_url }#{ regular_primary_resources_path }"
157
+ def singleton_related_url_helper_name(relationship)
158
+ relationship_parts = []
159
+ relationship_parts << relationship.name
160
+ relationship_parts += resource_path_parts_from_class(relationship.parent_resource)
161
+ url_helper_name_from_parts(relationship_parts)
140
162
  end
141
163
 
142
- def regular_resource_path(source)
143
- if source.is_a?(JSONAPI::CachedResponseFragment)
144
- # :nocov:
145
- "#{regular_resources_path(source.resource_klass)}/#{source.id}"
146
- # :nocov:
147
- else
148
- "#{regular_resources_path(source.class)}/#{source.id}"
149
- end
164
+ def relationship_self_url_helper_name(relationship)
165
+ relationship_parts = resource_path_parts_from_class(relationship.parent_resource)
166
+ relationship_parts << "relationships"
167
+ relationship_parts << relationship.name
168
+ url_helper_name_from_parts(relationship_parts)
150
169
  end
151
170
 
152
- def regular_resource_url(source)
153
- "#{ base_url }#{ regular_resource_path(source) }"
171
+ def singleton_relationship_self_url_helper_name(relationship)
172
+ relationship_parts = []
173
+ relationship_parts << "relationships"
174
+ relationship_parts << relationship.name
175
+ relationship_parts += resource_path_parts_from_class(relationship.parent_resource)
176
+ url_helper_name_from_parts(relationship_parts)
154
177
  end
155
178
 
156
- def route_for_relationship(relationship)
157
- format_route(relationship.name)
179
+ def module_scopes_from_class(klass)
180
+ klass.name.to_s.split("::")[0...-1]
158
181
  end
159
182
  end
160
183
  end
@@ -100,7 +100,7 @@ module JSONAPI
100
100
  end
101
101
  end
102
102
 
103
- class LinksObjectOperationResult < OperationResult
103
+ class RelationshipOperationResult < OperationResult
104
104
  attr_accessor :parent_resource, :relationship, :resource_ids
105
105
 
106
106
  def initialize(code, parent_resource, relationship, resource_ids, options = {})
@@ -112,7 +112,7 @@ module JSONAPI
112
112
 
113
113
  def to_hash(serializer = nil)
114
114
  if serializer
115
- serializer.serialize_to_links_hash(parent_resource, relationship, resource_ids)
115
+ serializer.serialize_to_relationship_hash(parent_resource, relationship, resource_ids)
116
116
  else
117
117
  # :nocov:
118
118
  {}
@@ -130,11 +130,11 @@ module JSONAPI
130
130
  find_options,
131
131
  nil)
132
132
 
133
- return JSONAPI::LinksObjectOperationResult.new(:ok,
134
- parent_resource,
135
- resource_klass._relationship(relationship_type),
136
- resource_id_tree.fragments.keys,
137
- result_options)
133
+ return JSONAPI::RelationshipOperationResult.new(:ok,
134
+ parent_resource,
135
+ resource_klass._relationship(relationship_type),
136
+ resource_id_tree.fragments.keys,
137
+ result_options)
138
138
  end
139
139
 
140
140
  def show_related_resource
@@ -429,8 +429,7 @@ module JSONAPI
429
429
  def load_included(resource_klass, source_resource_id_tree, include_related, options)
430
430
  source_rids = source_resource_id_tree.fragments.keys
431
431
 
432
- include_related.try(:each_pair) do |key, value|
433
- next unless value[:include]
432
+ include_related.try(:each_key) do |key|
434
433
  relationship = resource_klass._relationship(key)
435
434
  relationship_name = relationship.name.to_sym
436
435
 
@@ -27,7 +27,9 @@ module JSONAPI
27
27
  @class_name = nil
28
28
  @inverse_relationship = nil
29
29
 
30
- # Custom methods are reserved for use in resource finders. Not used in the default ActiveRelationResourceFinder
30
+ exclude_links(options.fetch(:exclude_links, :none))
31
+
32
+ # Custom methods are reserved for future use
31
33
  @custom_methods = options.fetch(:custom_methods, {})
32
34
  end
33
35
 
@@ -99,6 +101,27 @@ module JSONAPI
99
101
  @options[:readonly]
100
102
  end
101
103
 
104
+ def exclude_links(exclude)
105
+ case exclude
106
+ when :default, "default"
107
+ @_exclude_links = [:self, :related]
108
+ when :none, "none"
109
+ @_exclude_links = []
110
+ when Array
111
+ @_exclude_links = exclude.collect {|link| link.to_sym}
112
+ else
113
+ fail "Invalid exclude_links"
114
+ end
115
+ end
116
+
117
+ def _exclude_links
118
+ @_exclude_links ||= []
119
+ end
120
+
121
+ def exclude_link?(link)
122
+ _exclude_links.include?(link.to_sym)
123
+ end
124
+
102
125
  class ToOne < Relationship
103
126
  attr_reader :foreign_key_on
104
127
 
@@ -114,7 +137,7 @@ module JSONAPI
114
137
 
115
138
  def to_s
116
139
  # :nocov: useful for debugging
117
- "#{parent_resource._type}.#{name} => (#{belongs_to? ? 'ToOne' : 'BelongsToOne'}) #{resource_klass._type}"
140
+ "#{parent_resource}.#{name}(#{belongs_to? ? 'BelongsToOne' : 'ToOne'})"
118
141
  # :nocov:
119
142
  end
120
143
 
@@ -164,7 +187,7 @@ module JSONAPI
164
187
 
165
188
  def to_s
166
189
  # :nocov: useful for debugging
167
- "#{parent_resource._type}.#{name} => (ToMany) #{resource_klass._type}"
190
+ "#{parent_resource}.#{name}(ToMany)"
168
191
  # :nocov:
169
192
  end
170
193
 
@@ -81,6 +81,7 @@ module JSONAPI
81
81
  end
82
82
 
83
83
  def setup_show_related_resource_action(params, resource_klass)
84
+ resolve_singleton_id(params, resource_klass)
84
85
  source_klass = Resource.resource_klass_for(params.require(:source))
85
86
  source_id = source_klass.verify_key(params.require(source_klass._as_parent_key), @context)
86
87
 
@@ -102,6 +103,7 @@ module JSONAPI
102
103
  end
103
104
 
104
105
  def setup_index_related_resources_action(params, resource_klass)
106
+ resolve_singleton_id(params, resource_klass)
105
107
  source_klass = Resource.resource_klass_for(params.require(:source))
106
108
  source_id = source_klass.verify_key(params.require(source_klass._as_parent_key), @context)
107
109
 
@@ -128,6 +130,7 @@ module JSONAPI
128
130
  end
129
131
 
130
132
  def setup_show_action(params, resource_klass)
133
+ resolve_singleton_id(params, resource_klass)
131
134
  fields = parse_fields(resource_klass, params[:fields])
132
135
  include_directives = parse_include_directives(resource_klass, params[:include])
133
136
  id = params[:id]
@@ -144,6 +147,7 @@ module JSONAPI
144
147
  end
145
148
 
146
149
  def setup_show_relationship_action(params, resource_klass)
150
+ resolve_singleton_id(params, resource_klass)
147
151
  relationship_type = params[:relationship]
148
152
  parent_key = params.require(resource_klass._as_parent_key)
149
153
  include_directives = parse_include_directives(resource_klass, params[:include])
@@ -191,6 +195,7 @@ module JSONAPI
191
195
  end
192
196
 
193
197
  def setup_create_relationship_action(params, resource_klass)
198
+ resolve_singleton_id(params, resource_klass)
194
199
  parse_modify_relationship_action(:add, params, resource_klass)
195
200
  end
196
201
 
@@ -199,6 +204,7 @@ module JSONAPI
199
204
  end
200
205
 
201
206
  def setup_update_action(params, resource_klass)
207
+ resolve_singleton_id(params, resource_klass)
202
208
  fields = parse_fields(resource_klass, params[:fields])
203
209
  include_directives = parse_include_directives(resource_klass, params[:include])
204
210
 
@@ -232,6 +238,7 @@ module JSONAPI
232
238
  end
233
239
 
234
240
  def setup_destroy_action(params, resource_klass)
241
+ resolve_singleton_id(params, resource_klass)
235
242
  JSONAPI::Operation.new(
236
243
  :remove_resource,
237
244
  resource_klass,
@@ -240,6 +247,7 @@ module JSONAPI
240
247
  end
241
248
 
242
249
  def setup_destroy_relationship_action(params, resource_klass)
250
+ resolve_singleton_id(params, resource_klass)
243
251
  parse_modify_relationship_action(:remove, params, resource_klass)
244
252
  end
245
253
 
@@ -556,20 +564,39 @@ module JSONAPI
556
564
 
557
565
  links_object = parse_to_many_links_object(linkage)
558
566
 
559
- # Since we do not yet support polymorphic to_many relationships we will raise an error if the type does not match the
560
- # relationship's type.
561
- # ToDo: Support Polymorphic relationships
562
-
563
567
  if links_object.length == 0
564
568
  add_result.call([])
565
569
  else
566
- if links_object.length > 1 || !links_object.has_key?(unformat_key(relationship.type).to_s)
567
- fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type], error_object_overrides)
568
- end
570
+ if relationship.polymorphic?
571
+ polymorphic_results = []
572
+
573
+ links_object.each_pair do |type, keys|
574
+ type_name = unformat_key(type).to_s
575
+
576
+ relationship_resource_klass = resource_klass.resource_klass_for(relationship.class_name)
577
+ relationship_klass = relationship_resource_klass._model_class
569
578
 
570
- links_object.each_pair do |type, keys|
571
- relationship_resource = Resource.resource_klass_for(resource_klass.module_path + unformat_key(type).to_s)
572
- add_result.call relationship_resource.verify_keys(keys, @context)
579
+ linkage_object_resource_klass = resource_klass.resource_klass_for(type_name)
580
+ linkage_object_klass = linkage_object_resource_klass._model_class
581
+
582
+ unless linkage_object_klass == relationship_klass || linkage_object_klass.in?(relationship_klass.subclasses)
583
+ fail JSONAPI::Exceptions::TypeMismatch.new(type_name)
584
+ end
585
+
586
+ relationship_ids = relationship_resource_klass.verify_keys(keys, @context)
587
+ polymorphic_results << { type: type, ids: relationship_ids }
588
+ end
589
+
590
+ add_result.call polymorphic_results
591
+ else
592
+ relationship_type = unformat_key(relationship.type).to_s
593
+
594
+ if links_object.length > 1 || !links_object.has_key?(relationship_type)
595
+ fail JSONAPI::Exceptions::TypeMismatch.new(links_object[:type])
596
+ end
597
+
598
+ relationship_resource_klass = Resource.resource_klass_for(resource_klass.module_path + relationship_type)
599
+ add_result.call relationship_resource_klass.verify_keys(links_object[relationship_type], @context)
573
600
  end
574
601
  end
575
602
  end
@@ -695,6 +722,13 @@ module JSONAPI
695
722
  end
696
723
  end
697
724
 
725
+ def resolve_singleton_id(params, resource_klass)
726
+ if resource_klass.singleton? && params[:id].nil?
727
+ key = resource_klass.singleton_key(context)
728
+ params[:id] = key
729
+ end
730
+ end
731
+
698
732
  def format_key(key)
699
733
  @key_formatter.format(key)
700
734
  end