jsonapi-utils 0.6.0 → 0.7.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0efb4509e06e84372f0c8eb578bda2ee0c1996d7
4
- data.tar.gz: 0c6ed180ee94395c772150647a96caeb4067c08e
3
+ metadata.gz: fa0fb8cc338bd774c92dce69ce09c955e50449c0
4
+ data.tar.gz: 661f6402c7943d0ab6ce6ea6fc6c5f67260c2828
5
5
  SHA512:
6
- metadata.gz: 0aa6e06f2971a5ef29a353950c258ee883ddcf52d7398460edf5b3da622618c7cd7afd27d42c74df729cf03dedc56a0e0b7fbd6ed2a5a4ecad063c7a90007d67
7
- data.tar.gz: 88f627109d057c93192792df420ddd6948e332a98ca4f98d97a6772bbbaa319a0c5b139cb1b3c9c706eda7023fa84f7f0171c41833a018ca3a4bc3117349c133
6
+ metadata.gz: f7813a7385d284f6aba51027410645fae4b38563ca611482cde9fd5f4dde80fce225b8ccf82c34b9a43fcbcf2a825c757fbdbe1607c1dc62b1b715f2672f447e
7
+ data.tar.gz: cde1ac2b6cc5a5f43b0d7da7e8cac598d527821dde18dcbc9ee633acbece6411e5cefafbf81d5c4cc791af78abdf72e0f371e67c945aec4611fe8e91bb888d9a
data/README.md CHANGED
@@ -65,7 +65,7 @@ gem 'jsonapi-utils', '~> 0.4.9'
65
65
  For Rails 5:
66
66
 
67
67
  ```ruby
68
- gem 'jsonapi-utils', '~> 0.6.0.beta'
68
+ gem 'jsonapi-utils', '~> 0.7.0'
69
69
  ```
70
70
 
71
71
  And then execute:
@@ -110,8 +110,8 @@ end
110
110
 
111
111
  Arguments:
112
112
 
113
- - `json`: object to be rendered as a JSON document: ActiveRecord object, Hash or Array of Hashes;
114
- - `status`: HTTP status code (Integer or Symbol). If ommited a status code will be automatically infered;
113
+ - `json`: object to be rendered as a JSON document: ActiveRecord object, Hash or Array<Hash>;
114
+ - `status`: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered;
115
115
  - `options`:
116
116
  - `resource`: explicitly points the resource to be used in the serialization. By default, JU will select resources by inferencing from controller's name.
117
117
  - `count`: explicitly points the total count of records for the request in order to build a proper pagination. By default, JU will count the total number of records.
@@ -155,8 +155,8 @@ It renders a JSON API-compliant error response.
155
155
 
156
156
  Arguments:
157
157
  - Exception
158
- - `json`: object to be rendered as a JSON document: ActiveRecord, Exception, Array of Hashes or any object which implements the `errors` method;
159
- - `status`: HTTP status code (Integer or Symbol). If ommited a status code will be automatically infered from the error body.
158
+ - `json`: object to be rendered as a JSON document: ActiveRecord, Exception, Array<Hash> or any object which implements the `errors` method;
159
+ - `status`: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered from the error body.
160
160
 
161
161
  Other examples:
162
162
 
@@ -164,7 +164,7 @@ Other examples:
164
164
  # Render errors from a custom exception:
165
165
  jsonapi_render_errors Exceptions::MyCustomError.new(user)
166
166
 
167
- # Render errors from an Array of Hashes:
167
+ # Render errors from an Array<Hash>:
168
168
  errors = [{ id: 'validation', title: 'Something went wrong', code: '100' }]
169
169
  jsonapi_render_errors json: errors, status: :unprocessable_entity
170
170
  ```
@@ -188,7 +188,7 @@ end
188
188
  ```
189
189
 
190
190
  Arguments:
191
- - First: ActiveRecord object, Hash or Array of Hashes;
191
+ - First: ActiveRecord object, Hash or Array<Hash>;
192
192
  - Last: Hash of options (same as `JSONAPI::Utils#jsonapi_render`).
193
193
 
194
194
  #### Paginators
@@ -1,126 +1,2 @@
1
- require 'jsonapi/utils/version'
2
-
3
- module JSONAPI
4
- module Utils
5
- module Exceptions
6
- class ActiveRecord < ::JSONAPI::Exceptions::Error
7
- attr_reader :object, :resource, :relationships, :relationship_keys, :foreign_keys
8
-
9
- def initialize(object, resource_klass, context)
10
- @object = object
11
- @resource = resource_klass.new(object, context)
12
-
13
- # Need to reflect on resource's relationships for error reporting.
14
- @relationships = resource_klass._relationships.values
15
- @relationship_keys = @relationships.map(&:name).map(&:to_sym)
16
- @foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym)
17
- end
18
-
19
- def errors
20
- object.errors.messages.flat_map do |key, messages|
21
- messages.map do |message|
22
- error_meta = error_base
23
- .merge(title: title_member(key, message))
24
- .merge(id: id_member(key))
25
- .merge(source_member(key))
26
-
27
- JSONAPI::Error.new(error_meta)
28
- end
29
- end
30
- end
31
-
32
- private
33
-
34
- def id_member(key)
35
- id = resource_key_for(key)
36
- key_formatter = JSONAPI.configuration.key_formatter
37
- key_formatter.format(id).to_sym
38
- end
39
-
40
- # Determine if this is a foreign key, which will need to look up its
41
- # matching association name.
42
- def resource_key_for(key)
43
- if foreign_keys.include?(key)
44
- relationships.find { |r| r.foreign_key == key }.name.to_sym
45
- else
46
- key
47
- end
48
- end
49
-
50
- def source_member(key)
51
- Hash.new.tap do |hash|
52
- resource_key = resource_key_for(key)
53
-
54
- # Pointer should only be created for whitelisted attributes.
55
- return hash unless resource.fetchable_fields.include?(resource_key) || key == :base
56
-
57
- id = id_member(key)
58
-
59
- hash[:source] = {}
60
- hash[:source][:pointer] =
61
- # Relationship
62
- if relationship_keys.include?(resource_key)
63
- "/data/relationships/#{id}"
64
- # Base
65
- elsif key == :base
66
- '/data'
67
- # Attribute
68
- else
69
- "/data/attributes/#{id}"
70
- end
71
- end
72
- end
73
-
74
- def title_member(key, message)
75
- if key == :base
76
- message
77
- else
78
- resource_key = resource_key_for(key)
79
- [translation_for(resource_key), message].join(' ')
80
- end
81
- end
82
-
83
- def translation_for(key)
84
- object.class.human_attribute_name(key)
85
- end
86
-
87
- def error_base
88
- {
89
- code: JSONAPI::VALIDATION_ERROR,
90
- status: :unprocessable_entity
91
- }
92
- end
93
- end
94
-
95
- class BadRequest < ::JSONAPI::Exceptions::Error
96
- def code
97
- '400'
98
- end
99
-
100
- def errors
101
- [JSONAPI::Error.new(
102
- code: code,
103
- status: :bad_request,
104
- title: 'Bad Request',
105
- detail: 'This request is not supported.'
106
- )]
107
- end
108
- end
109
-
110
- class InternalServerError < ::JSONAPI::Exceptions::Error
111
- def code
112
- '500'
113
- end
114
-
115
- def errors
116
- [JSONAPI::Error.new(
117
- code: code,
118
- status: :internal_server_error,
119
- title: 'Internal Server Error',
120
- detail: 'An internal error ocurred while processing the request.'
121
- )]
122
- end
123
- end
124
- end
125
- end
126
- end
1
+ require 'jsonapi/utils/exceptions/active_record'
2
+ require 'jsonapi/utils/exceptions/internal_server_error'
@@ -0,0 +1,197 @@
1
+ module JSONAPI
2
+ module Utils
3
+ module Exceptions
4
+ class ActiveRecord < ::JSONAPI::Exceptions::Error
5
+ attr_reader :object, :resource, :relationships, :relationship_names, :foreign_keys
6
+
7
+ # Construct an error decorator over ActiveRecord objects.
8
+ #
9
+ # @param object [ActiveRecord::Base] Invalid ActiveRecord object.
10
+ # e.g.: User.new(name: nil).tap(&:save)
11
+ #
12
+ # @param resource_klass [JSONAPI::Resource] Resource class to be used for reflection.
13
+ # e.g.: UserResuource
14
+ #
15
+ # @return [JSONAPI::Utils::Exceptions::ActiveRecord]
16
+ #
17
+ # @api public
18
+ def initialize(object, resource_klass, context)
19
+ @object = object
20
+ @resource = resource_klass.new(object, context)
21
+
22
+ # Need to reflect on resource's relationships for error reporting.
23
+ @relationships = resource_klass._relationships.values
24
+ @relationship_names = @relationships.map(&:name).map(&:to_sym)
25
+ @foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym)
26
+ @resource_key_for = {}
27
+ @formatted_key = {}
28
+ end
29
+
30
+ # Decorate errors for AR invalid objects.
31
+ #
32
+ # @note That's the method used by formatters to build the response's error body.
33
+ #
34
+ # @return [Array]
35
+ #
36
+ # @api public
37
+ def errors
38
+ object.errors.messages.flat_map do |field, messages|
39
+ messages.map.with_index do |message, index|
40
+ build_error(field, message, index)
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Turn AR error into JSONAPI::Error.
48
+ #
49
+ # @param field [Symbol] Name of the invalid field
50
+ # e.g.: :title
51
+ #
52
+ # @param message [String] Error message
53
+ # e.g.: "can't be blank"
54
+ #
55
+ # @param index [Integer] Index of the error detail
56
+ #
57
+ # @return [JSONAPI::Error]
58
+ #
59
+ # @api private
60
+ def build_error(field, message, index = 0)
61
+ error = error_base
62
+ .merge(
63
+ id: id_member(field, index),
64
+ title: message,
65
+ detail: detail_member(field, message)
66
+ ).merge(source_member(field))
67
+ JSONAPI::Error.new(error)
68
+ end
69
+
70
+ # Build the "id" member value for the JSON API error object.
71
+ # e.g.: for :first_name, :too_short => "first-name#too-short"
72
+ #
73
+ # @note The returned value depends on the key formatter type defined
74
+ # via configuration, e.g.: config.json_key_format = :dasherized_key
75
+ #
76
+ # @param field [Symbol] Name of the invalid field
77
+ # e.g.: :first_name
78
+ #
79
+ # @param index [Integer] Index of the error detail
80
+ #
81
+ # @return [String]
82
+ #
83
+ # @api private
84
+ def id_member(field, index)
85
+ [
86
+ key_format(field),
87
+ key_format(
88
+ object.errors.details
89
+ .dig(field, index, :error)
90
+ .to_s.downcase
91
+ .split
92
+ .join('_')
93
+ )
94
+ ].join('#')
95
+ end
96
+
97
+ # Bring the formatted resource key for a given field.
98
+ # e.g.: for :first_name => :"first-name"
99
+ #
100
+ # @note The returned value depends on the key formatter type defined
101
+ # via configuration, e.g.: config.json_key_format = :dasherized_key
102
+ #
103
+ # @param field [Symbol] Name of the invalid field
104
+ # e.g.: :title
105
+ #
106
+ # @return [Symbol]
107
+ #
108
+ # @api private
109
+ def key_format(field)
110
+ @formatted_key[field] ||= JSONAPI.configuration
111
+ .key_formatter
112
+ .format(resource_key_for(field))
113
+ .to_sym
114
+ end
115
+
116
+ # Build the "source" member value for the JSON API error object.
117
+ # e.g.: :title => "/data/attributes/title"
118
+ #
119
+ # @param field [Symbol] Name of the invalid field
120
+ # e.g.: :title
121
+ #
122
+ # @return [Hash]
123
+ #
124
+ # @api private
125
+ def source_member(field)
126
+ resource_key = resource_key_for(field)
127
+ return {} unless field == :base || resource.fetchable_fields.include?(resource_key)
128
+ id = key_format(field)
129
+
130
+ pointer =
131
+ if field == :base then '/data'
132
+ elsif relationship_names.include?(resource_key) then "/data/relationships/#{id}"
133
+ else "/data/attributes/#{id}"
134
+ end
135
+
136
+ { source: { pointer: pointer } }
137
+ end
138
+
139
+ # Build the "detail" member value for the JSON API error object.
140
+ # e.g.: :first_name, "can't be blank" => "First name can't be blank"
141
+ #
142
+ # @param field [Symbol] Name of the invalid field
143
+ # e.g.: :first_name
144
+ #
145
+ # @return [String]
146
+ #
147
+ # @api private
148
+ def detail_member(field, message)
149
+ return message if field == :base
150
+ resource_key = resource_key_for(field)
151
+ [translation_for(resource_key), message].join(' ')
152
+ end
153
+
154
+ # Return the resource's attribute or relationship key name for a given field name.
155
+ # e.g.: :title => :title, :user_id => :author
156
+ #
157
+ # @param field [Symbol] Name of the invalid field
158
+ # e.g.: :title
159
+ #
160
+ # @return [Symbol]
161
+ #
162
+ # @api private
163
+ def resource_key_for(field)
164
+ @resource_key_for[field] ||= begin
165
+ return field unless foreign_keys.include?(field)
166
+ relationships.find { |r| r.foreign_key == field }.name.to_sym
167
+ end
168
+ end
169
+
170
+ # Turn the field name into human-friendly one.
171
+ # e.g.: :first_name => "First name"
172
+ #
173
+ # @param field [Symbol] Name of the invalid field
174
+ # e.g.: :first_name
175
+ #
176
+ # @return [String]
177
+ #
178
+ # @api private
179
+ def translation_for(field)
180
+ object.class.human_attribute_name(field)
181
+ end
182
+
183
+ # Return the base data used for all errors of this kind.
184
+ #
185
+ # @return [Hash]
186
+ #
187
+ # @api private
188
+ def error_base
189
+ {
190
+ code: JSONAPI::VALIDATION_ERROR,
191
+ status: :unprocessable_entity
192
+ }
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,30 @@
1
+ module JSONAPI
2
+ module Utils
3
+ module Exceptions
4
+ class InternalServerError < ::JSONAPI::Exceptions::Error
5
+ # HTTP status code
6
+ #
7
+ # @return [String]
8
+ #
9
+ # @api public
10
+ def code
11
+ '500'
12
+ end
13
+
14
+ # Decorate errors for 500 responses.
15
+ #
16
+ # @return [Array]
17
+ #
18
+ # @api public
19
+ def errors
20
+ [JSONAPI::Error.new(
21
+ code: code,
22
+ status: :internal_server_error,
23
+ title: 'Internal Server Error',
24
+ detail: 'An internal error ocurred while processing the request.'
25
+ )]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,72 +2,168 @@ module JSONAPI
2
2
  module Utils
3
3
  module Response
4
4
  module Formatters
5
- def jsonapi_format(records, options = {})
6
- if records.is_a?(Hash)
7
- hash = records.with_indifferent_access
8
- records = hash_to_active_record(hash[:data], options[:model])
5
+ # Helper method to format ActiveRecord or Hash objects into JSON API-compliant ones.
6
+ #
7
+ # @note The return of this method represents what will actually be displayed in the response body.
8
+ # @note It can also be called as #jsonapi_serialize due to backward compatibility issues.
9
+ #
10
+ # @param object [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array<Hash>]
11
+ # Object to be formatted into JSON
12
+ # e.g.: User.first, User.all, { data: { id: 1, first_name: 'Tiago' } },
13
+ # [{ data: { id: 1, first_name: 'Tiago' } }]
14
+ #
15
+ # @option options [JSONAPI::Resource] resource: it tells the formatter which resource
16
+ # class to be used rather than use an infered one (default behaviour)
17
+ #
18
+ # @option options [ActiveRecord::Base] model: ActiveRecord model class to be instantiated
19
+ # when a Hash or Array of Hashes is passed as the "object" argument
20
+ #
21
+ # @option options [Integer] count: if it's rendering a collection of resources, the default
22
+ # gem's counting method can be bypassed by the use of this options. It's shows then the total
23
+ # records resulting from that request and also calculates the pagination.
24
+ #
25
+ # @return [Hash]
26
+ #
27
+ # @api public
28
+ def jsonapi_format(object, options = {})
29
+ if object.is_a?(Hash)
30
+ hash = object.with_indifferent_access
31
+ object = hash_to_active_record(hash[:data], options[:model])
9
32
  end
10
- fix_request_options(params, records)
11
- build_response_document(records, options).contents
33
+ fix_custom_request_options(object)
34
+ build_response_document(object, options).contents
12
35
  end
13
36
 
14
37
  alias_method :jsonapi_serialize, :jsonapi_format
15
38
 
16
- def jsonapi_format_errors(data)
17
- data = JSONAPI::Utils::Exceptions::ActiveRecord.new(data, @request.resource_klass, context) if active_record_obj?(data)
18
- errors = data.respond_to?(:errors) ? data.errors : data
39
+ # Helper method to format ActiveRecord or any object that responds to #errors
40
+ # into JSON API-compliant error response bodies.
41
+ #
42
+ # @note The return of this method represents what will actually be displayed in the response body.
43
+ # @note It can also be called as #jsonapi_serialize_errors due to backward compatibility issues.
44
+ #
45
+ # @param object [ActiveRecord::Base or any object that responds to #errors]
46
+ # Error object to be serialized into JSON
47
+ # e.g.: User.new(name: nil).tap(&:save), MyErrorDecorator.new(invalid_object)
48
+ #
49
+ # @return [Array]
50
+ #
51
+ # @api public
52
+ def jsonapi_format_errors(object)
53
+ if active_record_obj?(object)
54
+ object = JSONAPI::Utils::Exceptions::ActiveRecord.new(object, @request.resource_klass, context)
55
+ end
56
+ errors = object.respond_to?(:errors) ? object.errors : object
19
57
  JSONAPI::Utils::Support::Error.sanitize(errors).uniq
20
58
  end
21
59
 
60
+ alias_method :jsonapi_serialize_errors, :jsonapi_format_errors
61
+
22
62
  private
23
63
 
24
- def active_record_obj?(data)
25
- defined?(ActiveRecord::Base) && (data.is_a?(ActiveRecord::Base) || data.singleton_class.include?(ActiveModel::Model))
64
+ # Check whether the given object is an ActiveRecord-like one.
65
+ #
66
+ # @param object [Object] Object to be checked
67
+ #
68
+ # @return [TrueClass, FalseClass]
69
+ #
70
+ # @api private
71
+ def active_record_obj?(object)
72
+ defined?(ActiveRecord::Base) &&
73
+ (object.is_a?(ActiveRecord::Base) ||
74
+ object.singleton_class.include?(ActiveModel::Model))
26
75
  end
27
76
 
28
- def build_response_document(records, options)
77
+ # Build the full response document.
78
+ #
79
+ # @param object [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array<Hash>]
80
+ # Object to be formatted into JSON
81
+ # e.g.: User.first, User.all, { data: { id: 1, first_name: 'Tiago' } },
82
+ # [{ data: { id: 1, first_name: 'Tiago' } }]
83
+ #
84
+ # @option options [JSONAPI::Resource] resource: it tells the builder which resource
85
+ # class to be used rather than use an infered one (default behaviour)
86
+ #
87
+ # @option options [Integer] count: if it's rendering a collection of resources, the default
88
+ # gem's counting method can be bypassed by the use of this options. It's shows then the total
89
+ # records resulting from that request and also calculates the pagination.
90
+ #
91
+ # @return [JSONAPI::ResponseDocument]
92
+ #
93
+ # @api private
94
+ def build_response_document(object, options)
29
95
  results = JSONAPI::OperationResults.new
30
96
 
31
- if records.respond_to?(:to_ary)
32
- @_records = build_collection(records, options)
33
- results.add_result(JSONAPI::ResourcesOperationResult.new(:ok, @_records, result_options(records, options)))
97
+ if object.respond_to?(:to_ary)
98
+ records = build_collection(object, options)
99
+ results.add_result(JSONAPI::ResourcesOperationResult.new(:ok, records, result_options(object, options)))
34
100
  else
35
- @_record = turn_into_resource(records, options)
36
- results.add_result(JSONAPI::ResourceOperationResult.new(:ok, @_record))
101
+ record = turn_into_resource(object, options)
102
+ results.add_result(JSONAPI::ResourceOperationResult.new(:ok, record))
37
103
  end
38
104
 
39
105
  @_response_document = create_response_document(results)
40
106
  end
41
107
 
42
- def fix_request_options(params, records)
43
- return if request.method !~ /get/i ||
44
- params.nil? ||
45
- %w(index show create update destroy).include?(params[:action])
46
- action = records.respond_to?(:to_ary) ? 'index' : 'show'
108
+ # Apply a proper action setup for custom requests/actions.
109
+ #
110
+ # @note The setup_(index|show)_action comes from JSONAPI::Resources' API.
111
+ #
112
+ # @param object [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array<Hash>]
113
+ # It's checked whether this object refers to a collection or not.
114
+ #
115
+ # @api private
116
+ def fix_custom_request_options(object)
117
+ return unless custom_get_request_with_params?
118
+ action = object.respond_to?(:to_ary) ? 'index' : 'show'
47
119
  @request.send("setup_#{action}_action", params)
48
120
  end
49
121
 
50
- def result_options(records, options)
51
- {}.tap do |data|
52
- if JSONAPI.configuration.default_paginator != :none &&
53
- JSONAPI.configuration.top_level_links_include_pagination
54
- data[:pagination_params] = pagination_params(records, options)
55
- end
56
-
57
- if JSONAPI.configuration.top_level_meta_include_record_count
58
- data[:record_count] = count_records(records, options)
59
- end
60
- end
122
+ # Check whether it's a custom GET request with params.
123
+ #
124
+ # @return [TrueClass, FalseClass]
125
+ #
126
+ # @api private
127
+ def custom_get_request_with_params?
128
+ request.method =~ /get/i && !%w(index show).include?(params[:action]) && !params.nil?
61
129
  end
62
130
 
63
- def build_collection(records, options = {})
131
+ # Turn a collection of AR or Hash objects into a collection of JSONAPI::Resource ones.
132
+ #
133
+ # @param records [ActiveRecord::Relation, Hash, Array<Hash>]
134
+ # Objects to be instantiated as JSONAPI::Resource ones.
135
+ # e.g.: User.all, [{ data: { id: 1, first_name: 'Tiago' } }]
136
+ #
137
+ # @option options [JSONAPI::Resource] resource: it tells the buider which resource
138
+ # class to be used rather than use an infered one (default behaviour)
139
+ #
140
+ # @option options [Integer] count: if it's rendering a collection of resources, the default
141
+ # gem's counting method can be bypassed by the use of this options. It's shows then the total
142
+ # records resulting from that request and also calculates the pagination.
143
+ #
144
+ # @return [Array]
145
+ #
146
+ # @api private
147
+ def build_collection(records, options)
64
148
  records = apply_filter(records, options)
65
- records = apply_pagination(records, options)
66
149
  records = apply_sort(records)
150
+ records = apply_pagination(records, options)
67
151
  records.respond_to?(:to_ary) ? records.map { |record| turn_into_resource(record, options) } : []
68
152
  end
69
153
 
70
- def turn_into_resource(record, options = {})
154
+ # Turn an AR or Hash object into a JSONAPI::Resource one.
155
+ #
156
+ # @param records [ActiveRecord::Relation, Hash, Array<Hash>]
157
+ # Object to be instantiated as a JSONAPI::Resource one.
158
+ # e.g.: User.first, { data: { id: 1, first_name: 'Tiago' } }
159
+ #
160
+ # @option options [JSONAPI::Resource] resource: it tells which resource
161
+ # class to be used rather than use an infered one (default behaviour)
162
+ #
163
+ # @return [JSONAPI::Resource]
164
+ #
165
+ # @api private
166
+ def turn_into_resource(record, options)
71
167
  if options[:resource]
72
168
  options[:resource].to_s.constantize.new(record, context)
73
169
  else
@@ -75,6 +171,44 @@ module JSONAPI
75
171
  end
76
172
  end
77
173
 
174
+ # Apply some result options like pagination params and count to a collection response.
175
+ #
176
+ # @param records [ActiveRecord::Relation, Hash, Array<Hash>]
177
+ # Object to be formatted into JSON
178
+ # e.g.: User.all, [{ data: { id: 1, first_name: 'Tiago' } }]
179
+ #
180
+ # @option options [Integer] count: if it's rendering a collection of resources, the default
181
+ # gem's counting method can be bypassed by the use of this options. It's shows then the total
182
+ # records resulting from that request and also calculates the pagination.
183
+ #
184
+ # @return [Hash]
185
+ #
186
+ # @api private
187
+ def result_options(records, options)
188
+ {}.tap do |data|
189
+ if JSONAPI.configuration.default_paginator != :none &&
190
+ JSONAPI.configuration.top_level_links_include_pagination
191
+ data[:pagination_params] = pagination_params(records, options)
192
+ end
193
+
194
+ if JSONAPI.configuration.top_level_meta_include_record_count
195
+ data[:record_count] = count_records(records, options)
196
+ end
197
+ end
198
+ end
199
+
200
+ # Convert Hash or collection of Hashes into AR objects.
201
+ #
202
+ # @param data [Hash, Array<Hash>] Hash or collection to be converted
203
+ # e.g.: { data: { id: 1, first_name: 'Tiago' } },
204
+ # [{ data: { id: 1, first_name: 'Tiago' } }],
205
+ #
206
+ # @option options [ActiveRecord::Base] model: ActiveRecord model class to be
207
+ # used as base for the objects' intantialization.
208
+ #
209
+ # @return [ActiveRecord::Base, ActiveRecord::Relation]
210
+ #
211
+ # @api private
78
212
  def hash_to_active_record(data, model)
79
213
  return data if model.nil?
80
214
  coerced = [data].flatten.map { |hash| model.new(hash) }
@@ -2,36 +2,87 @@ module JSONAPI
2
2
  module Utils
3
3
  module Response
4
4
  module Renders
5
+ # Helper method to render JSON API-compliant responses.
6
+ #
7
+ # @param json [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array<Hash>]
8
+ # Object to be serialized into JSON
9
+ # e.g.: User.first, User.all, { data: { id: 1, first_name: 'Tiago' } },
10
+ # [{ data: { id: 1, first_name: 'Tiago' } }]
11
+ #
12
+ # @param status [Integer, String, Symbol] HTTP status code
13
+ # e.g.: 201, '201', :created
14
+ #
15
+ # @option options [JSONAPI::Resource] resource: it tells the render which resource
16
+ # class to be used rather than use an infered one (default behaviour)
17
+ #
18
+ # @option options [ActiveRecord::Base] model: ActiveRecord model class to be instantiated
19
+ # when a Hash or Array of Hashes is passed to the "json" key argument
20
+ #
21
+ # @option options [Integer] count: if it's rendering a collection of resources, the default
22
+ # gem's counting method can be bypassed by the use of this options. It's shows then the total
23
+ # records resulting from that request and also calculates the pagination.
24
+ #
25
+ # @return [String]
26
+ #
27
+ # @api public
5
28
  def jsonapi_render(json:, status: nil, options: {})
6
29
  body = jsonapi_format(json, options)
7
- render json: body, status: status || @_response_document.status
30
+ render json: body, status: (status || @_response_document.status)
8
31
  rescue => e
9
- handle_exceptions(e)
32
+ handle_exceptions(e) # http://bit.ly/2sEEGTN
10
33
  ensure
11
34
  correct_media_type
12
35
  end
13
36
 
14
- def jsonapi_render_errors(exception = nil, json: nil, status: nil)
15
- body = jsonapi_format_errors(exception || json)
16
- status = status || body.try(:first).try(:[], :status)
37
+ # Helper method to render JSON API-compliant error responses.
38
+ #
39
+ # @param error [ActiveRecord::Base or any object that responds to #errors]
40
+ # Error object to be serialized into JSON
41
+ # e.g.: User.new(name: nil).tap(&:save), MyErrorDecorator.new(invalid_object)
42
+ #
43
+ # @param json [ActiveRecord::Base or any object that responds to #errors]
44
+ # Error object to be serialized into JSON
45
+ # e.g.: User.new(name: nil).tap(&:save), MyErrorDecorator.new(invalid_object)
46
+ #
47
+ # @param status [Integer, String, Symbol] HTTP status code
48
+ # e.g.: 422, '422', :unprocessable_entity
49
+ #
50
+ # @return [String]
51
+ #
52
+ # @api public
53
+ def jsonapi_render_errors(error = nil, json: nil, status: nil)
54
+ body = jsonapi_format_errors(error || json)
55
+ status = status || body.try(:first).try(:[], :status) || :bad_request
17
56
  render json: { errors: body }, status: status
18
57
  ensure
19
58
  correct_media_type
20
59
  end
21
60
 
61
+ # Helper method to render HTTP 500 Interval Server Error.
62
+ #
63
+ # @api public
22
64
  def jsonapi_render_internal_server_error
23
65
  jsonapi_render_errors(::JSONAPI::Utils::Exceptions::InternalServerError.new)
24
66
  end
25
67
 
68
+ # Helper method to render HTTP 400 Bad Request.
69
+ #
70
+ # @api public
26
71
  def jsonapi_render_bad_request
27
72
  jsonapi_render_errors(::JSONAPI::Utils::Exceptions::BadRequest.new)
28
73
  end
29
74
 
75
+ # Helper method to render HTTP 404 Bad Request.
76
+ #
77
+ # @api public
30
78
  def jsonapi_render_not_found(exception)
31
- id = exception.message.match(/=([\w-]+)/).try(:[], 1) || '(no identifier)'
79
+ id = exception.message =~ /=([\w-]+)/ && $1 || '(no identifier)'
32
80
  jsonapi_render_errors(JSONAPI::Exceptions::RecordNotFound.new(id))
33
81
  end
34
82
 
83
+ # Helper method to render HTTP 404 Bad Request with null "data".
84
+ #
85
+ # @api public
35
86
  def jsonapi_render_not_found_with_null
36
87
  render json: { data: nil }, status: 200
37
88
  end
@@ -43,9 +43,9 @@ module JSONAPI::Utils::Support
43
43
  @_sort_params ||=
44
44
  if params[:sort].present?
45
45
  params[:sort].split(',').each_with_object({}) do |field, hash|
46
- unformatted_field = @request.unformat_key(field)
47
- desc, field = unformatted_field.to_s.match(/^([-_])?(\w+)$/i)[1..2]
48
- hash[field] = desc.present? ? :desc : :asc
46
+ unformatted_field = @request.unformat_key(field)
47
+ desc, field = unformatted_field.to_s.match(/^([-_])?(\w+)$/i)[1..2]
48
+ hash[field.to_sym] = desc.present? ? :desc : :asc
49
49
  end
50
50
  end
51
51
  end
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Utils
3
- VERSION = '0.6.0'.freeze
3
+ VERSION = '0.7.0'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi-utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Guedes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2017-05-23 00:00:00.000000000 Z
12
+ date: 2017-07-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: jsonapi-resources
@@ -54,33 +54,33 @@ dependencies:
54
54
  - !ruby/object:Gem::Version
55
55
  version: '10.0'
56
56
  - !ruby/object:Gem::Dependency
57
- name: sqlite3
57
+ name: rails
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - ">="
60
+ - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: '0'
62
+ version: '5.1'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - ">="
67
+ - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '0'
69
+ version: '5.1'
70
70
  - !ruby/object:Gem::Dependency
71
- name: rails
71
+ name: sqlite3
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
- - - '='
74
+ - - ">="
75
75
  - !ruby/object:Gem::Version
76
- version: 5.0.1
76
+ version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
- - - '='
81
+ - - ">="
82
82
  - !ruby/object:Gem::Version
83
- version: 5.0.1
83
+ version: '0'
84
84
  - !ruby/object:Gem::Dependency
85
85
  name: rspec-rails
86
86
  requirement: !ruby/object:Gem::Requirement
@@ -115,14 +115,14 @@ dependencies:
115
115
  requirements:
116
116
  - - "~>"
117
117
  - !ruby/object:Gem::Version
118
- version: 0.1.5
118
+ version: 0.1.6
119
119
  type: :development
120
120
  prerelease: false
121
121
  version_requirements: !ruby/object:Gem::Requirement
122
122
  requirements:
123
123
  - - "~>"
124
124
  - !ruby/object:Gem::Version
125
- version: 0.1.5
125
+ version: 0.1.6
126
126
  - !ruby/object:Gem::Dependency
127
127
  name: pry
128
128
  requirement: !ruby/object:Gem::Requirement
@@ -167,6 +167,8 @@ files:
167
167
  - bin/setup
168
168
  - lib/jsonapi/utils.rb
169
169
  - lib/jsonapi/utils/exceptions.rb
170
+ - lib/jsonapi/utils/exceptions/active_record.rb
171
+ - lib/jsonapi/utils/exceptions/internal_server_error.rb
170
172
  - lib/jsonapi/utils/request.rb
171
173
  - lib/jsonapi/utils/response.rb
172
174
  - lib/jsonapi/utils/response/formatters.rb