jsonapi-resources 0.9.12 → 0.10.0.beta1

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 (36) hide show
  1. checksums.yaml +5 -5
  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-resources.rb +8 -3
  7. data/lib/jsonapi/active_relation_resource_finder.rb +640 -0
  8. data/lib/jsonapi/active_relation_resource_finder/join_tree.rb +126 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +121 -106
  10. data/lib/jsonapi/{cached_resource_fragment.rb → cached_response_fragment.rb} +13 -30
  11. data/lib/jsonapi/compiled_json.rb +11 -1
  12. data/lib/jsonapi/configuration.rb +44 -18
  13. data/lib/jsonapi/error.rb +27 -0
  14. data/lib/jsonapi/exceptions.rb +43 -40
  15. data/lib/jsonapi/formatter.rb +3 -3
  16. data/lib/jsonapi/include_directives.rb +2 -45
  17. data/lib/jsonapi/link_builder.rb +87 -80
  18. data/lib/jsonapi/operation.rb +16 -5
  19. data/lib/jsonapi/operation_result.rb +74 -16
  20. data/lib/jsonapi/processor.rb +233 -112
  21. data/lib/jsonapi/relationship.rb +77 -53
  22. data/lib/jsonapi/request_parser.rb +378 -423
  23. data/lib/jsonapi/resource.rb +224 -524
  24. data/lib/jsonapi/resource_controller_metal.rb +2 -2
  25. data/lib/jsonapi/resource_fragment.rb +47 -0
  26. data/lib/jsonapi/resource_id_tree.rb +112 -0
  27. data/lib/jsonapi/resource_identity.rb +42 -0
  28. data/lib/jsonapi/resource_serializer.rb +133 -301
  29. data/lib/jsonapi/resource_set.rb +108 -0
  30. data/lib/jsonapi/resources/version.rb +1 -1
  31. data/lib/jsonapi/response_document.rb +100 -88
  32. data/lib/jsonapi/routing_ext.rb +21 -43
  33. metadata +29 -45
  34. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  35. data/lib/jsonapi/operation_results.rb +0 -35
  36. data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -108,7 +108,7 @@ end
108
108
 
109
109
  class DasherizedKeyFormatter < JSONAPI::KeyFormatter
110
110
  class << self
111
- def format(key)
111
+ def format(_key)
112
112
  super.underscore.dasherize
113
113
  end
114
114
 
@@ -146,7 +146,7 @@ end
146
146
 
147
147
  class CamelizedRouteFormatter < JSONAPI::RouteFormatter
148
148
  class << self
149
- def format(route)
149
+ def format(_route)
150
150
  super.camelize(:lower)
151
151
  end
152
152
 
@@ -158,7 +158,7 @@ end
158
158
 
159
159
  class DasherizedRouteFormatter < JSONAPI::RouteFormatter
160
160
  class << self
161
- def format(route)
161
+ def format(_route)
162
162
  super.dasherize
163
163
  end
164
164
 
@@ -19,9 +19,8 @@ module JSONAPI
19
19
  # }
20
20
  # }
21
21
 
22
- def initialize(resource_klass, includes_array, force_eager_load: false)
22
+ def initialize(resource_klass, includes_array)
23
23
  @resource_klass = resource_klass
24
- @force_eager_load = force_eager_load
25
24
  @include_directives_hash = { include_related: {} }
26
25
  includes_array.each do |include|
27
26
  parse_include(include)
@@ -32,24 +31,6 @@ module JSONAPI
32
31
  @include_directives_hash
33
32
  end
34
33
 
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
34
  private
54
35
 
55
36
  def get_related(current_path)
@@ -65,24 +46,13 @@ module JSONAPI
65
46
  raise JSONAPI::Exceptions::InvalidInclude.new(current_resource_klass, current_path)
66
47
  end
67
48
 
68
- include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include
69
49
 
70
- current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
50
+ current[:include_related][fragment] ||= { include: false, include_related: {} }
71
51
  current = current[:include_related][fragment]
72
52
  end
73
53
  current
74
54
  end
75
55
 
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
79
-
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
85
-
86
56
  def parse_include(include)
87
57
  parts = include.split('.')
88
58
  local_path = ''
@@ -93,18 +63,5 @@ module JSONAPI
93
63
  related[:include] = true
94
64
  end
95
65
  end
96
-
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
108
- end
109
66
  end
110
67
  end
@@ -3,100 +3,109 @@ module JSONAPI
3
3
  attr_reader :base_url,
4
4
  :primary_resource_klass,
5
5
  :route_formatter,
6
- :engine,
7
- :engine_mount_point,
8
- :url_helpers
9
-
10
- @@url_helper_methods = {}
6
+ :engine_name
11
7
 
12
8
  def initialize(config = {})
13
- @base_url = config[:base_url]
9
+ @base_url = config[:base_url]
14
10
  @primary_resource_klass = config[:primary_resource_klass]
15
- @route_formatter = config[:route_formatter]
16
- @engine = build_engine
17
- @engine_mount_point = @engine ? @engine.routes.find_script_name({}) : ""
11
+ @route_formatter = config[:route_formatter]
12
+ @engine_name = build_engine_name
18
13
 
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]
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)
18
+ end
23
19
  end
24
20
 
25
21
  def engine?
26
- !!@engine
22
+ !!@engine_name
27
23
  end
28
24
 
29
25
  def primary_resources_url
30
- if @primary_resource_klass._routed
31
- primary_resources_path = resources_path(primary_resource_klass)
32
- @primary_resources_url_cached ||= "#{ base_url }#{ serialized_engine_mount_point }#{ primary_resources_path }"
26
+ if engine?
27
+ engine_primary_resources_url
33
28
  else
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
29
+ regular_primary_resources_url
39
30
  end
40
31
  end
41
32
 
42
33
  def query_link(query_params)
43
- url = primary_resources_url
44
- return url if url.nil?
45
- "#{ url }?#{ query_params.to_query }"
34
+ "#{ primary_resources_url }?#{ query_params.to_query }"
46
35
  end
47
36
 
48
37
  def relationships_related_link(source, relationship, query_params = {})
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
38
+ url = "#{ self_link(source) }/#{ route_for_relationship(relationship) }"
39
+ url = "#{ url }?#{ query_params.to_query }" if query_params.present?
40
+ url
60
41
  end
61
42
 
62
43
  def relationships_self_link(source, relationship)
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
44
+ "#{ self_link(source) }/relationships/#{ route_for_relationship(relationship) }"
72
45
  end
73
46
 
74
47
  def self_link(source)
75
- if source.class._routed
76
- resource_url(source)
48
+ if engine?
49
+ engine_resource_url(source)
77
50
  else
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
51
+ regular_resource_url(source)
83
52
  end
84
53
  end
85
54
 
86
55
  private
87
56
 
88
- def build_engine
57
+ def build_engine_name
89
58
  scopes = module_scopes_from_class(primary_resource_klass)
90
59
 
91
60
  begin
92
61
  unless scopes.empty?
93
62
  "#{ scopes.first.to_s.camelize }::Engine".safe_constantize
94
63
  end
95
-
96
- # :nocov:
64
+ # :nocov:
97
65
  rescue LoadError => _e
98
66
  nil
99
- # :nocov:
67
+ # :nocov:
68
+ end
69
+ end
70
+
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)
74
+ end
75
+
76
+ def engine_primary_resources_path
77
+ engine_path_from_resource_class(primary_resource_klass)
78
+ end
79
+
80
+ def engine_primary_resources_url
81
+ "#{ base_url }#{ engine_primary_resources_path }"
82
+ end
83
+
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)
87
+ end
88
+
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("_")
94
+ end
95
+
96
+ def engine_resource_url(source)
97
+ "#{ base_url }#{ engine_resource_path(source) }"
98
+ end
99
+
100
+ def engine_resources_path_name_from_class(klass)
101
+ scopes = module_scopes_from_class(klass)[1..-1]
102
+ base_path_name = scopes.map { |scope| scope.underscore }.join("_")
103
+ 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"
100
109
  end
101
110
  end
102
111
 
@@ -105,19 +114,12 @@ module JSONAPI
105
114
  end
106
115
 
107
116
  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
117
+ scopes = module_scopes_from_class(klass)
115
118
 
116
- unless scopes.empty?
117
- "/#{ scopes.map {|scope| format_route(scope.to_s.underscore)}.compact.join('/') }/"
118
- else
119
- "/"
120
- end
119
+ unless scopes.empty?
120
+ "/#{ scopes.map{ |scope| format_route(scope.to_s.underscore) }.compact.join('/') }/"
121
+ else
122
+ "/"
121
123
  end
122
124
  end
123
125
 
@@ -125,25 +127,30 @@ module JSONAPI
125
127
  klass.name.to_s.split("::")[0...-1]
126
128
  end
127
129
 
128
- def resources_path(source_klass)
129
- formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
130
+ def regular_resources_path(source_klass)
131
+ @resources_path_cache.get(source_klass)
130
132
  end
131
133
 
132
- def resource_path(source)
133
- url = "#{resources_path(source.class)}"
134
+ def regular_primary_resources_path
135
+ regular_resources_path(primary_resource_klass)
136
+ end
134
137
 
135
- unless source.class.singleton?
136
- url = "#{url}/#{source.id}"
137
- end
138
- url
138
+ def regular_primary_resources_url
139
+ "#{ base_url }#{ regular_primary_resources_path }"
139
140
  end
140
141
 
141
- def resource_url(source)
142
- "#{ base_url }#{ serialized_engine_mount_point }#{ resource_path(source) }"
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
143
150
  end
144
151
 
145
- def serialized_engine_mount_point
146
- engine_mount_point == "/" ? "" : engine_mount_point
152
+ def regular_resource_url(source)
153
+ "#{ base_url }#{ regular_resource_path(source) }"
147
154
  end
148
155
 
149
156
  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
- class RelationshipOperationResult < OperationResult
57
- attr_accessor :parent_resource, :relationship
103
+ class LinksObjectOperationResult < 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_links_hash(parent_resource, relationship, resource_ids)
116
+ else
117
+ # :nocov:
118
+ {}
119
+ # :nocov:
120
+ end
121
+ end
64
122
  end
65
123
  end