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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 6c912f8bf18b7cb9aae401d121380b0d48e897d8
4
- data.tar.gz: ca83afd750f861b825c11e2f5b44de19532e252b
2
+ SHA256:
3
+ metadata.gz: 0c800b64bcd00613a30227d1ce18ad5a26e1cd28979fde5a64c055aa471864f9
4
+ data.tar.gz: 99fecc3f76f087018335941f017c7e04b5e95a9de4da4b21c5e7fd03302355cb
5
5
  SHA512:
6
- metadata.gz: 604a2e70580bfb24d1abe48a11fa6748128e05906ab324b13125addc6da362d36c2fd8371c6d14c06ddb5c13edf4bdd227a8474a2a21f90fb5256201f4c7c5eb
7
- data.tar.gz: 5441fb97181fb8cae74f7bfd99de4107cf4ef5a2b8bf766bbadba2dcacac4059639b0ac2344aa267a4def895c9cd247656eb22f36c1e1522843f00068da071bf
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://secure.travis-ci.org/jsonapi-rb/jsonapi-rails.svg?branch=master)](http://travis-ci.org/jsonapi-rb/jsonapi-rails?branch=master)
8
- [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/jsonapi-rb/Lobby)
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 = ->(_) { nil }
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
- "serializable_#{file_name}.rb")
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.name.underscore.pluralize
37
+ model_klass.model_name.plural
23
38
  end
24
39
 
25
40
  def attr_names
@@ -1,5 +1,5 @@
1
1
  <% module_namespacing do -%>
2
- class Serializable<%= class_name %> < JSONAPI::Serializable::Resource
2
+ class <%= serializable_class_name %> < JSONAPI::Serializable::Resource
3
3
  type '<%= type %>'
4
4
  <% attr_names.each do |attr| -%>
5
5
  attribute :<%= attr %>
@@ -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
- DEFAULT_JSONAPI_CLASS = Hash.new do |h, k|
8
- names = k.to_s.split('::')
9
- klass = names.pop
10
- h[k] = [*names, "Serializable#{klass}"].join('::').safe_constantize
11
- end.freeze
12
-
13
- DEFAULT_JSONAPI_ERRORS_CLASS = DEFAULT_JSONAPI_CLASS.dup.merge!(
14
- 'ActiveModel::Errors'.to_sym =>
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
- 'Hash'.to_sym => JSONAPI::Rails::SerializableErrorHash
17
- ).freeze
19
+ 'Hash'.to_sym => JSONAPI::Rails::SerializableErrorHash
20
+ ).freeze
18
21
 
19
- DEFAULT_JSONAPI_OBJECT = {
20
- version: '1.0'
21
- }.freeze
22
+ DEFAULT_JSONAPI_OBJECT = {
23
+ version: '1.0'
24
+ }.freeze
22
25
 
23
- DEFAULT_JSONAPI_EXPOSE = lambda {
24
- { url_helpers: ::Rails.application.routes.url_helpers }
25
- }.freeze
26
+ DEFAULT_JSONAPI_CACHE = ->() { nil }
26
27
 
27
- DEFAULT_JSONAPI_PAGINATION = ->(_) { nil }
28
+ DEFAULT_JSONAPI_EXPOSE = lambda {
29
+ { url_helpers: ::Rails.application.routes.url_helpers }
30
+ }
28
31
 
29
- DEFAULT_CONFIG = {
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
- def self.configure
38
- yield config
39
- end
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
- def self.config
42
- @config ||= JSONAPI::Rails::Configuration.new(DEFAULT_CONFIG)
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/deserializable'
2
- require 'jsonapi/parser'
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
- extend ActiveSupport::Concern
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'
@@ -0,0 +1,10 @@
1
+ module JSONAPI
2
+ module Rails
3
+ # @private
4
+ module Logging
5
+ def logger
6
+ config[:logger]
7
+ end
8
+ end
9
+ end
10
+ end
@@ -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
- self.content_type ||= Mime[:jsonapi]
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
- ActiveSupport::Notifications.instrument('render.jsonapi',
56
- resources: resources,
57
- options: options) do
58
- renderer.render(resources, options, self).to_json
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
- if (pagination_links = controller.jsonapi_pagination(resources))
25
- opts[:links] = (opts[:links] || {}).merge(pagination_links)
26
- end
27
- opts[:expose] = controller.jsonapi_expose.merge(opts[:expose] || {})
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
- @errors.keys.flat_map do |key|
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.3.1
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: 2017-09-11 00:00:00.000000000 Z
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: '0'
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: '0'
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
- rubyforge_project:
152
- rubygems_version: 2.6.13
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: []