jsonapi-resources 0.9.12 → 0.10.7

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