jsonapi-utils 0.6.0 → 0.7.0

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