jsonapi-resources 0.9.12 → 0.10.0.beta1

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