jsonapi-rails 0.3.1 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +2 -2
- data/lib/generators/jsonapi/initializer/templates/initializer.rb +46 -1
- data/lib/generators/jsonapi/serializable/serializable_generator.rb +18 -3
- data/lib/generators/jsonapi/serializable/templates/serializable.rb.erb +1 -1
- data/lib/jsonapi/rails/configuration.rb +51 -29
- data/lib/jsonapi/rails/controller/deserialization.rb +84 -0
- data/lib/jsonapi/rails/controller/hooks.rb +75 -0
- data/lib/jsonapi/rails/controller.rb +4 -117
- data/lib/jsonapi/rails/deserializable_resource.rb +21 -0
- data/lib/jsonapi/rails/filter_media_type.rb +39 -0
- data/lib/jsonapi/rails/log_subscriber.rb +25 -0
- data/lib/jsonapi/rails/logging.rb +10 -0
- data/lib/jsonapi/rails/railtie.rb +76 -8
- data/lib/jsonapi/rails/renderer.rb +12 -4
- data/lib/jsonapi/rails/serializable_active_model_errors.rb +7 -1
- data/lib/jsonapi/rails.rb +10 -1
- metadata +32 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0c800b64bcd00613a30227d1ce18ad5a26e1cd28979fde5a64c055aa471864f9
|
4
|
+
data.tar.gz: 99fecc3f76f087018335941f017c7e04b5e95a9de4da4b21c5e7fd03302355cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23a5535c16e86c274045d93dcdccfe86731247b96afb432c1945a0bd3043e497ac8df0e8bd69d2436d19e5a54d3b7867586827bf0ab804a4f4c27dbfb4e287e0
|
7
|
+
data.tar.gz: 1cc3c9b6305fe6829fb44caa19716a429ea51a0210a080c64c1668d7e5e1aa9d29ae645ac2adc69a4b92329b6d0bd7d875ea4bf2563941b8f6561ba8458fd920
|
data/README.md
CHANGED
@@ -4,8 +4,8 @@ Rails integration for [jsonapi-rb](http://jsonapi-rb.org).
|
|
4
4
|
## Status
|
5
5
|
|
6
6
|
[![Gem Version](https://badge.fury.io/rb/jsonapi-rails.svg)](https://badge.fury.io/rb/jsonapi-rails)
|
7
|
-
[![Build Status](https://
|
8
|
-
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.
|
7
|
+
[![Build Status](https://github.com/jsonapi-rb/jsonapi-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/jsonapi-rb/jsonapi-rails/actions/workflows/ci.yml/badge.svg)
|
8
|
+
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/jsonapi-rb/Lobby)
|
9
9
|
|
10
10
|
## Resources
|
11
11
|
|
@@ -21,12 +21,57 @@ JSONAPI::Rails.configure do |config|
|
|
21
21
|
# version: '1.0'
|
22
22
|
# }
|
23
23
|
#
|
24
|
+
# # Set default cache.
|
25
|
+
# # A lambda/proc that will be eval'd in the controller context.
|
26
|
+
# config.jsonapi_cache = ->() { nil }
|
27
|
+
#
|
28
|
+
# # Uncomment the following to enable fragment caching. Make sure you
|
29
|
+
# # invalidate cache keys accordingly.
|
30
|
+
# config.jsonapi_cache = lambda {
|
31
|
+
# Rails.cache
|
32
|
+
# }
|
33
|
+
#
|
24
34
|
# # Set default exposures.
|
25
35
|
# # A lambda/proc that will be eval'd in the controller context.
|
26
36
|
# config.jsonapi_expose = lambda {
|
27
37
|
# { url_helpers: ::Rails.application.routes.url_helpers }
|
28
38
|
# }
|
29
39
|
#
|
40
|
+
# # Set default fields.
|
41
|
+
# # A lambda/proc that will be eval'd in the controller context.
|
42
|
+
# config.jsonapi_fields = ->() { nil }
|
43
|
+
#
|
44
|
+
# # Uncomment the following to have it default to the `fields` query
|
45
|
+
# # parameter.
|
46
|
+
# config.jsonapi_fields = lambda {
|
47
|
+
# fields_param = params.to_unsafe_hash.fetch(:fields, {})
|
48
|
+
# Hash[fields_param.map { |k, v| [k.to_sym, v.split(',').map!(&:to_sym)] }]
|
49
|
+
# }
|
50
|
+
#
|
51
|
+
# # Set default include.
|
52
|
+
# # A lambda/proc that will be eval'd in the controller context.
|
53
|
+
# config.jsonapi_include = ->() { nil }
|
54
|
+
#
|
55
|
+
# # Uncomment the following to have it default to the `include` query
|
56
|
+
# # parameter.
|
57
|
+
# config.jsonapi_include = lambda {
|
58
|
+
# params[:include]
|
59
|
+
# }
|
60
|
+
#
|
61
|
+
# # Set default links.
|
62
|
+
# # A lambda/proc that will be eval'd in the controller context.
|
63
|
+
# config.jsonapi_links = ->() { {} }
|
64
|
+
#
|
65
|
+
# # Set default meta.
|
66
|
+
# # A lambda/proc that will be eval'd in the controller context.
|
67
|
+
# config.jsonapi_meta = ->() { nil }
|
68
|
+
#
|
30
69
|
# # Set a default pagination scheme.
|
31
|
-
# config.jsonapi_pagination = ->(_) {
|
70
|
+
# config.jsonapi_pagination = ->(_) { {} }
|
71
|
+
#
|
72
|
+
# # Set a logger.
|
73
|
+
# config.logger = Logger.new(STDOUT)
|
74
|
+
#
|
75
|
+
# # Uncomment the following to disable logging.
|
76
|
+
# config.logger = Logger.new('/dev/null')
|
32
77
|
end
|
@@ -6,20 +6,35 @@ module Jsonapi
|
|
6
6
|
# TODO(beauby): Implement versioning.
|
7
7
|
|
8
8
|
def copy_serializable_file
|
9
|
+
fail "#{class_name} model not found." unless model_exists?
|
10
|
+
|
9
11
|
template 'serializable.rb.erb',
|
10
12
|
File.join('app/serializable', class_path,
|
11
|
-
"
|
13
|
+
"#{serializable_file_name}.rb")
|
12
14
|
end
|
13
15
|
|
14
16
|
private
|
15
17
|
|
18
|
+
def model_exists?
|
19
|
+
Rails.application.eager_load!
|
20
|
+
models = ApplicationRecord.descendants.map(&:name)
|
21
|
+
!!models.find { |model_name| model_name == class_name }
|
22
|
+
end
|
23
|
+
|
24
|
+
def serializable_file_name
|
25
|
+
"serializable_#{file_name}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def serializable_class_name
|
29
|
+
(class_path + [serializable_file_name]).map!(&:camelize).join("::")
|
30
|
+
end
|
31
|
+
|
16
32
|
def model_klass
|
17
|
-
# TODO(beauby): Ensure the model class exists.
|
18
33
|
class_name.safe_constantize
|
19
34
|
end
|
20
35
|
|
21
36
|
def type
|
22
|
-
model_klass.
|
37
|
+
model_klass.model_name.plural
|
23
38
|
end
|
24
39
|
|
25
40
|
def attr_names
|
@@ -4,42 +4,64 @@ require 'jsonapi/rails/serializable_error_hash'
|
|
4
4
|
module JSONAPI
|
5
5
|
module Rails
|
6
6
|
class Configuration < ActiveSupport::InheritableOptions; end
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
|
8
|
+
# @private
|
9
|
+
module Configurable
|
10
|
+
DEFAULT_JSONAPI_CLASS = Hash.new do |h, k|
|
11
|
+
names = k.to_s.split('::')
|
12
|
+
klass = names.pop
|
13
|
+
h[k] = [*names, "Serializable#{klass}"].join('::').safe_constantize
|
14
|
+
end.freeze
|
15
|
+
|
16
|
+
DEFAULT_JSONAPI_ERRORS_CLASS = DEFAULT_JSONAPI_CLASS.dup.merge!(
|
17
|
+
'ActiveModel::Errors'.to_sym =>
|
15
18
|
JSONAPI::Rails::SerializableActiveModelErrors,
|
16
|
-
|
17
|
-
|
19
|
+
'Hash'.to_sym => JSONAPI::Rails::SerializableErrorHash
|
20
|
+
).freeze
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
+
DEFAULT_JSONAPI_OBJECT = {
|
23
|
+
version: '1.0'
|
24
|
+
}.freeze
|
22
25
|
|
23
|
-
|
24
|
-
{ url_helpers: ::Rails.application.routes.url_helpers }
|
25
|
-
}.freeze
|
26
|
+
DEFAULT_JSONAPI_CACHE = ->() { nil }
|
26
27
|
|
27
|
-
|
28
|
+
DEFAULT_JSONAPI_EXPOSE = lambda {
|
29
|
+
{ url_helpers: ::Rails.application.routes.url_helpers }
|
30
|
+
}
|
28
31
|
|
29
|
-
|
30
|
-
jsonapi_class: DEFAULT_JSONAPI_CLASS,
|
31
|
-
jsonapi_errors_class: DEFAULT_JSONAPI_ERRORS_CLASS,
|
32
|
-
jsonapi_object: DEFAULT_JSONAPI_OBJECT,
|
33
|
-
jsonapi_expose: DEFAULT_JSONAPI_EXPOSE,
|
34
|
-
jsonapi_pagination: DEFAULT_JSONAPI_PAGINATION
|
35
|
-
}.freeze
|
32
|
+
DEFAULT_JSONAPI_FIELDS = ->() { nil }
|
36
33
|
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
DEFAULT_JSONAPI_INCLUDE = ->() { nil }
|
35
|
+
|
36
|
+
DEFAULT_JSONAPI_LINKS = ->() { {} }
|
37
|
+
|
38
|
+
DEFAULT_JSONAPI_META = ->() { nil }
|
39
|
+
|
40
|
+
DEFAULT_JSONAPI_PAGINATION = ->(_) { {} }
|
41
|
+
|
42
|
+
DEFAULT_LOGGER = Logger.new(STDERR)
|
43
|
+
|
44
|
+
DEFAULT_CONFIG = {
|
45
|
+
jsonapi_class: DEFAULT_JSONAPI_CLASS,
|
46
|
+
jsonapi_errors_class: DEFAULT_JSONAPI_ERRORS_CLASS,
|
47
|
+
jsonapi_cache: DEFAULT_JSONAPI_CACHE,
|
48
|
+
jsonapi_expose: DEFAULT_JSONAPI_EXPOSE,
|
49
|
+
jsonapi_fields: DEFAULT_JSONAPI_FIELDS,
|
50
|
+
jsonapi_include: DEFAULT_JSONAPI_INCLUDE,
|
51
|
+
jsonapi_links: DEFAULT_JSONAPI_LINKS,
|
52
|
+
jsonapi_meta: DEFAULT_JSONAPI_META,
|
53
|
+
jsonapi_object: DEFAULT_JSONAPI_OBJECT,
|
54
|
+
jsonapi_pagination: DEFAULT_JSONAPI_PAGINATION,
|
55
|
+
logger: DEFAULT_LOGGER
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
def configure
|
59
|
+
yield config
|
60
|
+
end
|
40
61
|
|
41
|
-
|
42
|
-
|
62
|
+
def config
|
63
|
+
@config ||= JSONAPI::Rails::Configuration.new(DEFAULT_CONFIG)
|
64
|
+
end
|
43
65
|
end
|
44
66
|
end
|
45
67
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'jsonapi/parser'
|
2
|
+
require 'jsonapi/rails/deserializable_resource'
|
3
|
+
|
4
|
+
module JSONAPI
|
5
|
+
module Rails
|
6
|
+
module Controller
|
7
|
+
# Controller class and instance methods for deserialization of incoming
|
8
|
+
# JSON API payloads.
|
9
|
+
module Deserialization
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
JSONAPI_POINTERS_KEY = 'jsonapi-rails.jsonapi_pointers'.freeze
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
# Declare a deserializable resource.
|
16
|
+
#
|
17
|
+
# @param key [Symbol] The key under which the deserialized hash will be
|
18
|
+
# available within the `params` hash.
|
19
|
+
# @param options [Hash]
|
20
|
+
# @option class [Class] A custom deserializer class. Optional.
|
21
|
+
# @option only List of actions for which deserialization should happen.
|
22
|
+
# Optional.
|
23
|
+
# @option except List of actions for which deserialization should not
|
24
|
+
# happen. Optional.
|
25
|
+
# @yieldreturn Optional block for in-line definition of custom
|
26
|
+
# deserializers.
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# class ArticlesController < ActionController::Base
|
30
|
+
# deserializable_resource :article, only: [:create, :update]
|
31
|
+
#
|
32
|
+
# def create
|
33
|
+
# article = Article.new(params[:article])
|
34
|
+
#
|
35
|
+
# if article.save
|
36
|
+
# render jsonapi: article
|
37
|
+
# else
|
38
|
+
# render jsonapi_errors: article.errors
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# # ...
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
46
|
+
def deserializable_resource(key, options = {}, &block)
|
47
|
+
options = options.dup
|
48
|
+
klass = options.delete(:class) ||
|
49
|
+
Class.new(JSONAPI::Rails::DeserializableResource, &block)
|
50
|
+
|
51
|
+
before_action(options) do |controller|
|
52
|
+
hash = controller.params.to_unsafe_hash
|
53
|
+
.with_indifferent_access[:_jsonapi]
|
54
|
+
if hash.nil?
|
55
|
+
JSONAPI::Rails.logger.warn do
|
56
|
+
"Unable to deserialize #{key} because no JSON API payload was" \
|
57
|
+
" found. (#{controller.controller_name}##{params[:action]})"
|
58
|
+
end
|
59
|
+
next
|
60
|
+
end
|
61
|
+
|
62
|
+
ActiveSupport::Notifications
|
63
|
+
.instrument('parse.jsonapi-rails',
|
64
|
+
key: key, payload: hash, class: klass) do
|
65
|
+
JSONAPI::Parser::Resource.parse!(hash)
|
66
|
+
resource = klass.new(hash[:data])
|
67
|
+
controller.request.env[JSONAPI_POINTERS_KEY] =
|
68
|
+
resource.reverse_mapping
|
69
|
+
controller.params[key.to_sym] = resource.to_hash
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
74
|
+
end
|
75
|
+
|
76
|
+
# JSON pointers for deserialized fields.
|
77
|
+
# @return [Hash{Symbol=>String}]
|
78
|
+
def jsonapi_pointers
|
79
|
+
request.env[JSONAPI_POINTERS_KEY] || {}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'jsonapi/rails/configuration'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
module Rails
|
5
|
+
module Controller
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
# Hooks for customizing rendering default options at controller-level.
|
9
|
+
module Hooks
|
10
|
+
# Hook for serializable class mapping (for resources).
|
11
|
+
# Overridden by the `class` renderer option.
|
12
|
+
# @return [Hash{Symbol=>Class}]
|
13
|
+
def jsonapi_class
|
14
|
+
JSONAPI::Rails.config[:jsonapi_class].dup
|
15
|
+
end
|
16
|
+
|
17
|
+
# Hook for serializable class mapping (for errors).
|
18
|
+
# Overridden by the `class` renderer option.
|
19
|
+
# @return [Hash{Symbol=>Class}]
|
20
|
+
def jsonapi_errors_class
|
21
|
+
JSONAPI::Rails.config[:jsonapi_errors_class].dup
|
22
|
+
end
|
23
|
+
|
24
|
+
# Hook for the jsonapi object.
|
25
|
+
# Overridden by the `jsonapi_object` renderer option.
|
26
|
+
# @return [Hash,nil]
|
27
|
+
def jsonapi_object
|
28
|
+
JSONAPI::Rails.config[:jsonapi_object]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Hook for default exposures.
|
32
|
+
# @return [Hash]
|
33
|
+
def jsonapi_expose
|
34
|
+
instance_exec(&JSONAPI::Rails.config[:jsonapi_expose])
|
35
|
+
end
|
36
|
+
|
37
|
+
# Hook for default cache.
|
38
|
+
# @return [#fetch_multi]
|
39
|
+
def jsonapi_cache
|
40
|
+
instance_exec(&JSONAPI::Rails.config[:jsonapi_cache])
|
41
|
+
end
|
42
|
+
|
43
|
+
# Hook for default fields.
|
44
|
+
# @return [Hash{Symbol=>Array<Symbol>},nil]
|
45
|
+
def jsonapi_fields
|
46
|
+
instance_exec(&JSONAPI::Rails.config[:jsonapi_fields])
|
47
|
+
end
|
48
|
+
|
49
|
+
# Hook for default includes.
|
50
|
+
# @return [IncludeDirective]
|
51
|
+
def jsonapi_include
|
52
|
+
instance_exec(&JSONAPI::Rails.config[:jsonapi_include])
|
53
|
+
end
|
54
|
+
|
55
|
+
# Hook for default links.
|
56
|
+
# @return [Hash]
|
57
|
+
def jsonapi_links
|
58
|
+
instance_exec(&JSONAPI::Rails.config[:jsonapi_links])
|
59
|
+
end
|
60
|
+
|
61
|
+
# Hook for default meta.
|
62
|
+
# @return [Hash,nil]
|
63
|
+
def jsonapi_meta
|
64
|
+
instance_exec(&JSONAPI::Rails.config[:jsonapi_meta])
|
65
|
+
end
|
66
|
+
|
67
|
+
# Hook for pagination scheme.
|
68
|
+
# @return [Hash]
|
69
|
+
def jsonapi_pagination(resources)
|
70
|
+
instance_exec(resources, &JSONAPI::Rails.config[:jsonapi_pagination])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -1,126 +1,13 @@
|
|
1
|
-
require 'jsonapi/
|
2
|
-
require 'jsonapi/
|
3
|
-
require 'jsonapi/rails/configuration'
|
1
|
+
require 'jsonapi/rails/controller/deserialization'
|
2
|
+
require 'jsonapi/rails/controller/hooks'
|
4
3
|
|
5
4
|
module JSONAPI
|
6
5
|
module Rails
|
7
|
-
module Deserializable
|
8
|
-
# @private
|
9
|
-
class Resource < JSONAPI::Deserializable::Resource
|
10
|
-
id
|
11
|
-
type
|
12
|
-
attributes
|
13
|
-
has_one do |_rel, id, type, key|
|
14
|
-
type = type.to_s.singularize.camelize
|
15
|
-
{ "#{key}_id".to_sym => id, "#{key}_type".to_sym => type }
|
16
|
-
end
|
17
|
-
has_many do |_rel, ids, types, key|
|
18
|
-
key = key.to_s.singularize
|
19
|
-
types = types.map { |t| t.to_s.singularize.camelize }
|
20
|
-
{ "#{key}_ids".to_sym => ids, "#{key}_types".to_sym => types }
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
6
|
# ActionController methods and hooks for JSON API deserialization and
|
26
7
|
# rendering.
|
27
8
|
module Controller
|
28
|
-
|
29
|
-
|
30
|
-
JSONAPI_POINTERS_KEY = 'jsonapi-rails.jsonapi_pointers'.freeze
|
31
|
-
|
32
|
-
class_methods do
|
33
|
-
# Declare a deserializable resource.
|
34
|
-
#
|
35
|
-
# @param key [Symbol] The key under which the deserialized hash will be
|
36
|
-
# available within the `params` hash.
|
37
|
-
# @param options [Hash]
|
38
|
-
# @option class [Class] A custom deserializer class. Optional.
|
39
|
-
# @option only List of actions for which deserialization should happen.
|
40
|
-
# Optional.
|
41
|
-
# @option except List of actions for which deserialization should not
|
42
|
-
# happen. Optional.
|
43
|
-
# @yieldreturn Optional block for in-line definition of custom
|
44
|
-
# deserializers.
|
45
|
-
#
|
46
|
-
# @example
|
47
|
-
# class ArticlesController < ActionController::Base
|
48
|
-
# deserializable_resource :article, only: [:create, :update]
|
49
|
-
#
|
50
|
-
# def create
|
51
|
-
# article = Article.new(params[:article])
|
52
|
-
#
|
53
|
-
# if article.save
|
54
|
-
# render jsonapi: article
|
55
|
-
# else
|
56
|
-
# render jsonapi_errors: article.errors
|
57
|
-
# end
|
58
|
-
# end
|
59
|
-
#
|
60
|
-
# # ...
|
61
|
-
# end
|
62
|
-
#
|
63
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
64
|
-
def deserializable_resource(key, options = {}, &block)
|
65
|
-
options = options.dup
|
66
|
-
klass = options.delete(:class) ||
|
67
|
-
Class.new(JSONAPI::Rails::Deserializable::Resource, &block)
|
68
|
-
|
69
|
-
before_action(options) do |controller|
|
70
|
-
# TODO(lucas): Fail with helpful error message if _jsonapi not
|
71
|
-
# present.
|
72
|
-
hash = controller.params[:_jsonapi].to_unsafe_hash
|
73
|
-
ActiveSupport::Notifications
|
74
|
-
.instrument('parse.jsonapi', payload: hash, class: klass) do
|
75
|
-
JSONAPI::Parser::Resource.parse!(hash)
|
76
|
-
resource = klass.new(hash[:data])
|
77
|
-
controller.request.env[JSONAPI_POINTERS_KEY] =
|
78
|
-
resource.reverse_mapping
|
79
|
-
controller.params[key.to_sym] = resource.to_hash
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
84
|
-
end
|
85
|
-
|
86
|
-
# Hook for serializable class mapping (for resources).
|
87
|
-
# Overridden by the `class` renderer option.
|
88
|
-
# @return [Hash{Symbol=>Class}]
|
89
|
-
def jsonapi_class
|
90
|
-
JSONAPI::Rails.config[:jsonapi_class].dup
|
91
|
-
end
|
92
|
-
|
93
|
-
# Hook for serializable class mapping (for errors).
|
94
|
-
# Overridden by the `class` renderer option.
|
95
|
-
# @return [Hash{Symbol=>Class}]
|
96
|
-
def jsonapi_errors_class
|
97
|
-
JSONAPI::Rails.config[:jsonapi_errors_class].dup
|
98
|
-
end
|
99
|
-
|
100
|
-
# Hook for the jsonapi object.
|
101
|
-
# Overridden by the `jsonapi_object` renderer option.
|
102
|
-
# @return [Hash]
|
103
|
-
def jsonapi_object
|
104
|
-
JSONAPI::Rails.config[:jsonapi_object]
|
105
|
-
end
|
106
|
-
|
107
|
-
# Hook for default exposures.
|
108
|
-
# @return [Hash]
|
109
|
-
def jsonapi_expose
|
110
|
-
instance_exec(&JSONAPI::Rails.config[:jsonapi_expose])
|
111
|
-
end
|
112
|
-
|
113
|
-
# Hook for pagination scheme.
|
114
|
-
# @return [Hash]
|
115
|
-
def jsonapi_pagination(resources)
|
116
|
-
instance_exec(resources, &JSONAPI::Rails.config[:jsonapi_pagination])
|
117
|
-
end
|
118
|
-
|
119
|
-
# JSON pointers for deserialized fields.
|
120
|
-
# @return [Hash{Symbol=>String}]
|
121
|
-
def jsonapi_pointers
|
122
|
-
request.env[JSONAPI_POINTERS_KEY] || {}
|
123
|
-
end
|
9
|
+
include Deserialization
|
10
|
+
include Hooks
|
124
11
|
end
|
125
12
|
end
|
126
13
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'jsonapi/deserializable/resource'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
module Rails
|
5
|
+
# Customized deserializable resource class to match ActiveRecord's API.
|
6
|
+
class DeserializableResource < JSONAPI::Deserializable::Resource
|
7
|
+
id
|
8
|
+
type
|
9
|
+
attributes
|
10
|
+
has_one do |_rel, id, type, key|
|
11
|
+
type = type.to_s.singularize.camelize
|
12
|
+
{ "#{key}_id".to_sym => id, "#{key}_type".to_sym => type }
|
13
|
+
end
|
14
|
+
has_many do |_rel, ids, types, key|
|
15
|
+
key = key.to_s.singularize
|
16
|
+
types = types.map { |t| t.to_s.singularize.camelize }
|
17
|
+
{ "#{key}_ids".to_sym => ids, "#{key}_types".to_sym => types }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rack/media_type'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
module Rails
|
5
|
+
class FilterMediaType
|
6
|
+
JSONAPI_MEDIA_TYPE = 'application/vnd.api+json'.freeze
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
return [415, {}, []] unless valid_content_type?(env['CONTENT_TYPE'])
|
14
|
+
return [406, {}, []] unless valid_accept?(env['HTTP_ACCEPT'])
|
15
|
+
|
16
|
+
@app.call(env)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def valid_content_type?(content_type)
|
22
|
+
Rack::MediaType.type(content_type) != JSONAPI_MEDIA_TYPE ||
|
23
|
+
content_type == JSONAPI_MEDIA_TYPE
|
24
|
+
end
|
25
|
+
|
26
|
+
def valid_accept?(accept)
|
27
|
+
return true if accept.nil?
|
28
|
+
|
29
|
+
jsonapi_media_types =
|
30
|
+
accept.split(',')
|
31
|
+
.map(&:strip)
|
32
|
+
.select { |m| Rack::MediaType.type(m) == JSONAPI_MEDIA_TYPE }
|
33
|
+
|
34
|
+
jsonapi_media_types.empty? ||
|
35
|
+
jsonapi_media_types.any? { |m| Rack::MediaType.params(m) == {} }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
module Rails
|
3
|
+
# @private
|
4
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
5
|
+
def render(event)
|
6
|
+
info do
|
7
|
+
"Completed JSON API rendering (#{event.duration.round(2)}ms)"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(event)
|
12
|
+
info do
|
13
|
+
"Completed JSON API deserializing of #{event.payload[:key]}" \
|
14
|
+
" (#{event.duration.round(2)})"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def logger
|
19
|
+
JSONAPI::Rails.logger
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
JSONAPI::Rails::LogSubscriber.attach_to :'jsonapi-rails'
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'rails/railtie'
|
2
|
-
require 'action_controller'
|
3
|
-
require 'active_support'
|
4
2
|
|
3
|
+
require 'jsonapi/rails/filter_media_type'
|
4
|
+
require 'jsonapi/rails/log_subscriber'
|
5
5
|
require 'jsonapi/rails/renderer'
|
6
6
|
|
7
7
|
module JSONAPI
|
@@ -20,7 +20,7 @@ module JSONAPI
|
|
20
20
|
jsonapi_errors: ErrorsRenderer.new
|
21
21
|
}.freeze
|
22
22
|
|
23
|
-
initializer 'jsonapi-rails.init' do
|
23
|
+
initializer 'jsonapi-rails.init' do |app|
|
24
24
|
register_mime_type
|
25
25
|
register_parameter_parser
|
26
26
|
register_renderers
|
@@ -28,6 +28,8 @@ module JSONAPI
|
|
28
28
|
require 'jsonapi/rails/controller'
|
29
29
|
include ::JSONAPI::Rails::Controller
|
30
30
|
end
|
31
|
+
|
32
|
+
app.middleware.use FilterMediaType
|
31
33
|
end
|
32
34
|
|
33
35
|
private
|
@@ -50,12 +52,78 @@ module JSONAPI
|
|
50
52
|
RENDERERS.each do |name, renderer|
|
51
53
|
::ActionController::Renderers.add(name) do |resources, options|
|
52
54
|
# Renderer proc is evaluated in the controller context.
|
53
|
-
|
55
|
+
headers['Content-Type'] = Mime[:jsonapi].to_s
|
56
|
+
|
57
|
+
ActiveSupport::Notifications.instrument(
|
58
|
+
'render.jsonapi-rails',
|
59
|
+
resources: resources,
|
60
|
+
options: options
|
61
|
+
) do
|
62
|
+
# Depending on whether or not a valid cache object is present
|
63
|
+
# in the options, the #render call below will return two
|
64
|
+
# slightly different kinds of hash.
|
65
|
+
#
|
66
|
+
# Both hashes have broadly the following structure, where r is
|
67
|
+
# some representation of a JSON::API resource:
|
68
|
+
#
|
69
|
+
# {
|
70
|
+
# data: [ r1, r2, r3 ],
|
71
|
+
# meta: { count: 12345 },
|
72
|
+
# jsonapi: { version: "1.0" }
|
73
|
+
# }
|
74
|
+
#
|
75
|
+
# For non-cached calls to this method, the `data` field in the
|
76
|
+
# return value will contain an array of Ruby hashes.
|
77
|
+
#
|
78
|
+
# For cached calls, the `data` field will contain an array of
|
79
|
+
# JSON strings corresponding to the same data. This happens
|
80
|
+
# because jsonapi-renderer caches both the JSON serialization
|
81
|
+
# step as well as the assembly of the relevant attributes into
|
82
|
+
# a JSON::API-compliant structure. Those JSON strings are
|
83
|
+
# created via calls to `to_json`. They are then wrapped in
|
84
|
+
# CachedResourcesProcessor::JSONString. This defines a
|
85
|
+
# `to_json` method which simply returns self, ie - it attempts
|
86
|
+
# to ensure that any further `to_json` calls result in no
|
87
|
+
# changes.
|
88
|
+
#
|
89
|
+
# That isn't what happens in a Rails context, however. Below,
|
90
|
+
# the last step is to convert the entire output hash of the
|
91
|
+
# renderer into a JSON string to send to the client. If we
|
92
|
+
# call `to_json` on the cached output, the already-made JSON
|
93
|
+
# strings in the `data` field will be converted again,
|
94
|
+
# resulting in malformed data reaching the client. This happens
|
95
|
+
# because the ActiveSupport `to_json` takes precedent, meaning
|
96
|
+
# the "no-op" `to_json` definition on JSONString never gets
|
97
|
+
# executed.
|
98
|
+
#
|
99
|
+
# We can get around this by using JSON.generate instead, which
|
100
|
+
# will use the `to_json` defined on JSONString rather than the
|
101
|
+
# ActiveSupport one.
|
102
|
+
#
|
103
|
+
# However, we can't use JSON.generate on the non-cached output.
|
104
|
+
# Doing so means that its `data` field contents are converted
|
105
|
+
# with a non-ActiveSupport `to_json`. This means cached and
|
106
|
+
# non-cached responses have subtle differences in how their
|
107
|
+
# resources are serialized. For example:
|
108
|
+
#
|
109
|
+
# x = Time.new(2021,1,1)
|
110
|
+
#
|
111
|
+
# x.to_json
|
112
|
+
# => "\"2021-01-01T00:00:00.000+00:00\""
|
113
|
+
#
|
114
|
+
# JSON.generate x
|
115
|
+
# => "\"2021-01-01 00:00:00 +0000\""
|
116
|
+
#
|
117
|
+
# The different outputs mean we need to take different
|
118
|
+
# approaches when converting the entire payload into JSON,
|
119
|
+
# hence the check below.
|
120
|
+
jsonapi_hash = renderer.render(resources, options, self)
|
54
121
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
122
|
+
if jsonapi_hash[:data]&.first&.class == JSONAPI::Renderer::CachedResourcesProcessor::JSONString
|
123
|
+
JSON.generate jsonapi_hash
|
124
|
+
else
|
125
|
+
jsonapi_hash.to_json
|
126
|
+
end
|
59
127
|
end
|
60
128
|
end
|
61
129
|
end
|
@@ -18,17 +18,24 @@ module JSONAPI
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
21
22
|
def default_options(options, controller, resources)
|
22
23
|
options.dup.tap do |opts|
|
23
24
|
opts[:class] ||= controller.jsonapi_class
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
opts[:cache] ||= controller.jsonapi_cache
|
26
|
+
opts[:links] =
|
27
|
+
controller.jsonapi_links
|
28
|
+
.merge!(controller.jsonapi_pagination(resources))
|
29
|
+
.merge!(opts[:links] || {})
|
30
|
+
opts[:expose] = controller.jsonapi_expose.merge!(opts[:expose] || {})
|
31
|
+
opts[:fields] ||= controller.jsonapi_fields
|
32
|
+
opts[:include] ||= controller.jsonapi_include
|
28
33
|
opts[:jsonapi] = opts.delete(:jsonapi_object) ||
|
29
34
|
controller.jsonapi_object
|
35
|
+
opts[:meta] ||= controller.jsonapi_meta
|
30
36
|
end
|
31
37
|
end
|
38
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
32
39
|
end
|
33
40
|
|
34
41
|
# @private
|
@@ -52,6 +59,7 @@ module JSONAPI
|
|
52
59
|
def default_options(options, controller)
|
53
60
|
options.dup.tap do |opts|
|
54
61
|
opts[:class] ||= controller.jsonapi_errors_class
|
62
|
+
opts[:links] = controller.jsonapi_links.merge!(opts[:links] || {})
|
55
63
|
opts[:expose] =
|
56
64
|
controller.jsonapi_expose
|
57
65
|
.merge(opts[:expose] || {})
|
@@ -25,7 +25,7 @@ module JSONAPI
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def as_jsonapi
|
28
|
-
|
28
|
+
error_keys.flat_map do |key|
|
29
29
|
@errors.full_messages_for(key).map do |message|
|
30
30
|
SerializableActiveModelError.new(field: key, message: message,
|
31
31
|
pointer: @reverse_mapping[key])
|
@@ -33,6 +33,12 @@ module JSONAPI
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def error_keys
|
40
|
+
@errors.respond_to?(:attribute_names) ? @errors.attribute_names : @errors.keys
|
41
|
+
end
|
36
42
|
end
|
37
43
|
end
|
38
44
|
end
|
data/lib/jsonapi/rails.rb
CHANGED
@@ -1,4 +1,13 @@
|
|
1
1
|
require 'jsonapi/deserializable'
|
2
2
|
require 'jsonapi/serializable'
|
3
|
-
require 'jsonapi/rails/configuration'
|
4
3
|
require 'jsonapi/rails/railtie'
|
4
|
+
|
5
|
+
module JSONAPI
|
6
|
+
module Rails
|
7
|
+
require 'jsonapi/rails/configuration'
|
8
|
+
require 'jsonapi/rails/logging'
|
9
|
+
|
10
|
+
extend Configurable
|
11
|
+
extend Logging
|
12
|
+
end
|
13
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lucas Hosseini
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jsonapi-rb
|
@@ -42,14 +42,14 @@ dependencies:
|
|
42
42
|
name: rails
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '5.0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
@@ -58,26 +58,26 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
61
|
+
version: '1.3'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
68
|
+
version: '1.3'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rake
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - "
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '11.3'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - "
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '11.3'
|
83
83
|
- !ruby/object:Gem::Dependency
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '3.5'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: with_model
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.0'
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
112
|
name: simplecov
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -125,6 +139,12 @@ files:
|
|
125
139
|
- lib/jsonapi/rails.rb
|
126
140
|
- lib/jsonapi/rails/configuration.rb
|
127
141
|
- lib/jsonapi/rails/controller.rb
|
142
|
+
- lib/jsonapi/rails/controller/deserialization.rb
|
143
|
+
- lib/jsonapi/rails/controller/hooks.rb
|
144
|
+
- lib/jsonapi/rails/deserializable_resource.rb
|
145
|
+
- lib/jsonapi/rails/filter_media_type.rb
|
146
|
+
- lib/jsonapi/rails/log_subscriber.rb
|
147
|
+
- lib/jsonapi/rails/logging.rb
|
128
148
|
- lib/jsonapi/rails/railtie.rb
|
129
149
|
- lib/jsonapi/rails/renderer.rb
|
130
150
|
- lib/jsonapi/rails/serializable_active_model_errors.rb
|
@@ -133,7 +153,7 @@ homepage: https://github.com/jsonapi-rb/jsonapi-rails
|
|
133
153
|
licenses:
|
134
154
|
- MIT
|
135
155
|
metadata: {}
|
136
|
-
post_install_message:
|
156
|
+
post_install_message:
|
137
157
|
rdoc_options: []
|
138
158
|
require_paths:
|
139
159
|
- lib
|
@@ -148,9 +168,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
148
168
|
- !ruby/object:Gem::Version
|
149
169
|
version: '0'
|
150
170
|
requirements: []
|
151
|
-
|
152
|
-
|
153
|
-
signing_key:
|
171
|
+
rubygems_version: 3.3.7
|
172
|
+
signing_key:
|
154
173
|
specification_version: 4
|
155
174
|
summary: jsonapi-rb integrations for Rails.
|
156
175
|
test_files: []
|