jsonapi-resources 0.10.0.beta3 → 0.10.0.beta4

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