jsonapi-resources 0.9.12 → 0.10.7

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +34 -11
  4. data/lib/bug_report_templates/rails_5_latest.rb +125 -0
  5. data/lib/bug_report_templates/rails_5_master.rb +140 -0
  6. data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +27 -0
  7. data/lib/jsonapi/active_relation/join_manager.rb +303 -0
  8. data/lib/jsonapi/active_relation_resource.rb +884 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +122 -106
  10. data/lib/jsonapi/basic_resource.rb +1162 -0
  11. data/lib/jsonapi/cached_response_fragment.rb +127 -0
  12. data/lib/jsonapi/compiled_json.rb +11 -1
  13. data/lib/jsonapi/configuration.rb +57 -8
  14. data/lib/jsonapi/error.rb +27 -0
  15. data/lib/jsonapi/error_codes.rb +2 -0
  16. data/lib/jsonapi/exceptions.rb +63 -40
  17. data/lib/jsonapi/formatter.rb +3 -3
  18. data/lib/jsonapi/include_directives.rb +18 -75
  19. data/lib/jsonapi/link_builder.rb +18 -25
  20. data/lib/jsonapi/operation.rb +16 -5
  21. data/lib/jsonapi/operation_result.rb +73 -15
  22. data/lib/jsonapi/paginator.rb +17 -0
  23. data/lib/jsonapi/path.rb +43 -0
  24. data/lib/jsonapi/path_segment.rb +76 -0
  25. data/lib/jsonapi/processor.rb +246 -111
  26. data/lib/jsonapi/relationship.rb +117 -24
  27. data/lib/jsonapi/request_parser.rb +383 -396
  28. data/lib/jsonapi/resource.rb +3 -1376
  29. data/lib/jsonapi/resource_controller_metal.rb +3 -0
  30. data/lib/jsonapi/resource_fragment.rb +47 -0
  31. data/lib/jsonapi/resource_id_tree.rb +112 -0
  32. data/lib/jsonapi/resource_identity.rb +42 -0
  33. data/lib/jsonapi/resource_serializer.rb +124 -286
  34. data/lib/jsonapi/resource_set.rb +176 -0
  35. data/lib/jsonapi/resources/railtie.rb +9 -0
  36. data/lib/jsonapi/resources/version.rb +1 -1
  37. data/lib/jsonapi/response_document.rb +104 -87
  38. data/lib/jsonapi/routing_ext.rb +19 -21
  39. data/lib/jsonapi-resources.rb +20 -4
  40. data/lib/tasks/check_upgrade.rake +52 -0
  41. metadata +34 -31
  42. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  43. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  44. data/lib/jsonapi/operation_results.rb +0 -35
  45. 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
- # 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
  # }
@@ -19,9 +17,8 @@ module JSONAPI
19
17
  # }
20
18
  # }
21
19
 
22
- def initialize(resource_klass, includes_array, force_eager_load: false)
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,79 +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
- def merge_filter(relation, filter)
44
- config = include_config(relation.to_sym)
45
- config[:include_filters] ||= {}
46
- config[:include_filters].merge!(filter)
47
- end
48
-
49
- def include_config(relation)
50
- @include_directives_hash[:include_related][relation]
51
- end
52
-
53
32
  private
54
33
 
55
- def get_related(current_path)
56
- current = @include_directives_hash
57
- current_resource_klass = @resource_klass
58
- current_path.split('.').each do |fragment|
59
- fragment = fragment.to_sym
60
-
61
- if current_resource_klass
62
- current_relationship = current_resource_klass._relationships[fragment]
63
- current_resource_klass = current_relationship.try(:resource_klass)
64
- else
65
- raise JSONAPI::Exceptions::InvalidInclude.new(current_resource_klass, current_path)
66
- end
67
-
68
- include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include
69
-
70
- current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
71
- current = current[:include_related][fragment]
72
- end
73
- current
74
- end
75
-
76
- def get_includes(directive, only_joined_includes = true)
77
- ir = directive[:include_related]
78
- 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)
79
39
 
80
- ir.map do |name, sub_directive|
81
- sub = get_includes(sub_directive, only_joined_includes)
82
- sub.any? ? { name => sub } : name
83
- end
84
- end
40
+ current = @include_directives_hash
85
41
 
86
- def parse_include(include)
87
- parts = include.split('.')
88
- local_path = ''
42
+ path.segments.each do |segment|
43
+ relationship_name = segment.relationship.name.to_sym
89
44
 
90
- parts.each do |name|
91
- local_path += local_path.length > 0 ? ".#{name}" : name
92
- related = get_related(local_path)
93
- related[:include] = true
45
+ current[:include_related][relationship_name] ||= { include_related: {} }
46
+ current = current[:include_related][relationship_name]
94
47
  end
95
- end
96
48
 
97
- def delve_paths(obj)
98
- case obj
99
- when Array
100
- obj.map{|elem| delve_paths(elem)}.flatten(1)
101
- when Hash
102
- obj.map{|k,v| [[k]] + delve_paths(v).map{|path| [k] + path } }.flatten(1)
103
- when Symbol, String
104
- [[obj]]
105
- else
106
- raise "delve_paths cannot descend into #{obj.class.name}"
107
- end
49
+ rescue JSONAPI::Exceptions::InvalidRelationship => _e
50
+ raise JSONAPI::Exceptions::InvalidInclude.new(@resource_klass, include)
108
51
  end
109
52
  end
110
53
  end
@@ -29,7 +29,7 @@ module JSONAPI
29
29
  def primary_resources_url
30
30
  if @primary_resource_klass._routed
31
31
  primary_resources_path = resources_path(primary_resource_klass)
32
- @primary_resources_url_cached ||= "#{ base_url }#{ serialized_engine_mount_point }#{ primary_resources_path }"
32
+ @primary_resources_url_cached ||= "#{ base_url }#{ engine_mount_point }#{ primary_resources_path }"
33
33
  else
34
34
  if JSONAPI.configuration.warn_on_missing_routes && !@primary_resource_klass._warned_missing_route
35
35
  warn "primary_resources_url for #{@primary_resource_klass} could not be generated"
@@ -105,19 +105,16 @@ module JSONAPI
105
105
  end
106
106
 
107
107
  def formatted_module_path_from_class(klass)
108
- @_module_path_cache ||= {}
109
- @_module_path_cache[klass] ||= begin
110
- scopes = if @engine
111
- module_scopes_from_class(klass)[1..-1]
112
- else
113
- module_scopes_from_class(klass)
114
- end
115
-
116
- unless scopes.empty?
117
- "/#{ scopes.map {|scope| format_route(scope.to_s.underscore)}.compact.join('/') }/"
118
- else
119
- "/"
120
- end
108
+ scopes = if @engine
109
+ module_scopes_from_class(klass)[1..-1]
110
+ else
111
+ module_scopes_from_class(klass)
112
+ end
113
+
114
+ unless scopes.empty?
115
+ "/#{ scopes.map {|scope| format_route(scope.to_s.underscore)}.compact.join('/') }/"
116
+ else
117
+ "/"
121
118
  end
122
119
  end
123
120
 
@@ -126,24 +123,20 @@ module JSONAPI
126
123
  end
127
124
 
128
125
  def resources_path(source_klass)
129
- formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
126
+ @_resources_path ||= {}
127
+ @_resources_path[source_klass] ||= formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
130
128
  end
131
129
 
132
130
  def resource_path(source)
133
- url = "#{resources_path(source.class)}"
134
-
135
- unless source.class.singleton?
136
- url = "#{url}/#{source.id}"
131
+ if source.class.singleton?
132
+ resources_path(source.class)
133
+ else
134
+ "#{resources_path(source.class)}/#{source.id}"
137
135
  end
138
- url
139
136
  end
140
137
 
141
138
  def resource_url(source)
142
- "#{ base_url }#{ serialized_engine_mount_point }#{ resource_path(source) }"
143
- end
144
-
145
- def serialized_engine_mount_point
146
- engine_mount_point == "/" ? "" : engine_mount_point
139
+ "#{ base_url }#{ engine_mount_point }#{ resource_path(source) }"
147
140
  end
148
141
 
149
142
  def route_for_relationship(relationship)
@@ -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
- JSONAPI::Processor.processor_instance_for(resource_klass, operation_type, options)
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 ResourceOperationResult < OperationResult
26
- attr_accessor :resource
41
+ class ResourceSetOperationResult < OperationResult
42
+ attr_accessor :resource_set, :pagination_params
27
43
 
28
- def initialize(code, resource, options = {})
29
- @resource = resource
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 ResourcesOperationResult < OperationResult
35
- attr_accessor :resources, :pagination_params, :record_count, :page_count
61
+ class ResourcesSetOperationResult < OperationResult
62
+ attr_accessor :resource_set, :pagination_params, :record_count, :page_count
36
63
 
37
- def initialize(code, resources, options = {})
38
- @resources = resources
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 RelatedResourcesOperationResult < ResourcesOperationResult
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, resources, options = {})
86
+ def initialize(code, source_resource, type, resource_set, options = {})
50
87
  @source_resource = source_resource
51
88
  @_type = type
52
- super(code, resources, options)
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
103
  class RelationshipOperationResult < OperationResult
57
- attr_accessor :parent_resource, :relationship
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
@@ -13,9 +13,16 @@ module JSONAPI
13
13
  # :nocov:
14
14
  end
15
15
 
16
+ def requires_record_count
17
+ # :nocov:
18
+ self.class.requires_record_count
19
+ # :nocov:
20
+ end
21
+
16
22
  class << self
17
23
  def requires_record_count
18
24
  # :nocov:
25
+ # @deprecated
19
26
  false
20
27
  # :nocov:
21
28
  end
@@ -36,10 +43,15 @@ class OffsetPaginator < JSONAPI::Paginator
36
43
  verify_pagination_params
37
44
  end
38
45
 
46
+ # @deprecated
39
47
  def self.requires_record_count
40
48
  true
41
49
  end
42
50
 
51
+ def requires_record_count
52
+ true
53
+ end
54
+
43
55
  def apply(relation, _order_options)
44
56
  relation.offset(@offset).limit(@limit)
45
57
  end
@@ -127,10 +139,15 @@ class PagedPaginator < JSONAPI::Paginator
127
139
  verify_pagination_params
128
140
  end
129
141
 
142
+ # @deprecated
130
143
  def self.requires_record_count
131
144
  true
132
145
  end
133
146
 
147
+ def requires_record_count
148
+ true
149
+ end
150
+
134
151
  def calculate_page_count(record_count)
135
152
  (record_count / @size.to_f).ceil
136
153
  end
@@ -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