File: deserialization.rb

package info (click to toggle)
ruby-active-model-serializers 0.10.12-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,752 kB
  • sloc: ruby: 13,138; sh: 53; makefile: 6
file content (215 lines) | stat: -rw-r--r-- 7,376 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# frozen_string_literal: true

module ActiveModelSerializers
  module Adapter
    class JsonApi
      # NOTE(Experimental):
      # This is an experimental feature. Both the interface and internals could be subject
      # to changes.
      module Deserialization
        InvalidDocument = Class.new(ArgumentError)

        module_function

        # Transform a JSON API document, containing a single data object,
        # into a hash that is ready for ActiveRecord::Base.new() and such.
        # Raises InvalidDocument if the payload is not properly formatted.
        #
        # @param [Hash|ActionController::Parameters] document
        # @param [Hash] options
        #   only: Array of symbols of whitelisted fields.
        #   except: Array of symbols of blacklisted fields.
        #   keys: Hash of translated keys (e.g. :author => :user).
        #   polymorphic: Array of symbols of polymorphic fields.
        # @return [Hash]
        #
        # @example
        #   document = {
        #     data: {
        #       id: 1,
        #       type: 'post',
        #       attributes: {
        #         title: 'Title 1',
        #         date: '2015-12-20'
        #       },
        #       associations: {
        #         author: {
        #           data: {
        #             type: 'user',
        #             id: 2
        #           }
        #         },
        #         second_author: {
        #           data: nil
        #         },
        #         comments: {
        #           data: [{
        #             type: 'comment',
        #             id: 3
        #           },{
        #             type: 'comment',
        #             id: 4
        #           }]
        #         }
        #       }
        #     }
        #   }
        #
        #   parse(document) #=>
        #     # {
        #     #   title: 'Title 1',
        #     #   date: '2015-12-20',
        #     #   author_id: 2,
        #     #   second_author_id: nil
        #     #   comment_ids: [3, 4]
        #     # }
        #
        #   parse(document, only: [:title, :date, :author],
        #                   keys: { date: :published_at },
        #                   polymorphic: [:author]) #=>
        #     # {
        #     #   title: 'Title 1',
        #     #   published_at: '2015-12-20',
        #     #   author_id: '2',
        #     #   author_type: 'people'
        #     # }
        #
        def parse!(document, options = {})
          parse(document, options) do |invalid_payload, reason|
            fail InvalidDocument, "Invalid payload (#{reason}): #{invalid_payload}"
          end
        end

        # Same as parse!, but returns an empty hash instead of raising InvalidDocument
        # on invalid payloads.
        def parse(document, options = {})
          document = document.dup.permit!.to_h if document.is_a?(ActionController::Parameters)

          validate_payload(document) do |invalid_document, reason|
            yield invalid_document, reason if block_given?
            return {}
          end

          primary_data = document['data']
          attributes = primary_data['attributes'] || {}
          attributes['id'] = primary_data['id'] if primary_data['id']
          relationships = primary_data['relationships'] || {}

          filter_fields(attributes, options)
          filter_fields(relationships, options)

          hash = {}
          hash.merge!(parse_attributes(attributes, options))
          hash.merge!(parse_relationships(relationships, options))

          hash
        end

        # Checks whether a payload is compliant with the JSON API spec.
        #
        # @api private
        # rubocop:disable Metrics/CyclomaticComplexity
        def validate_payload(payload)
          unless payload.is_a?(Hash)
            yield payload, 'Expected hash'
            return
          end

          primary_data = payload['data']
          unless primary_data.is_a?(Hash)
            yield payload, { data: 'Expected hash' }
            return
          end

          attributes = primary_data['attributes'] || {}
          unless attributes.is_a?(Hash)
            yield payload, { data: { attributes: 'Expected hash or nil' } }
            return
          end

          relationships = primary_data['relationships'] || {}
          unless relationships.is_a?(Hash)
            yield payload, { data: { relationships: 'Expected hash or nil' } }
            return
          end

          relationships.each do |(key, value)|
            unless value.is_a?(Hash) && value.key?('data')
              yield payload, { data: { relationships: { key => 'Expected hash with :data key' } } }
            end
          end
        end
        # rubocop:enable Metrics/CyclomaticComplexity

        # @api private
        def filter_fields(fields, options)
          if (only = options[:only])
            fields.slice!(*Array(only).map(&:to_s))
          elsif (except = options[:except])
            fields.except!(*Array(except).map(&:to_s))
          end
        end

        # @api private
        def field_key(field, options)
          (options[:keys] || {}).fetch(field.to_sym, field).to_sym
        end

        # @api private
        def parse_attributes(attributes, options)
          transform_keys(attributes, options)
            .map { |(k, v)| { field_key(k, options) => v } }
            .reduce({}, :merge)
        end

        # Given an association name, and a relationship data attribute, build a hash
        # mapping the corresponding ActiveRecord attribute to the corresponding value.
        #
        # @example
        #   parse_relationship(:comments, [{ 'id' => '1', 'type' => 'comments' },
        #                                  { 'id' => '2', 'type' => 'comments' }],
        #                                 {})
        #    # => { :comment_ids => ['1', '2'] }
        #   parse_relationship(:author, { 'id' => '1', 'type' => 'users' }, {})
        #    # => { :author_id => '1' }
        #   parse_relationship(:author, nil, {})
        #    # => { :author_id => nil }
        # @param [Symbol] assoc_name
        # @param [Hash] assoc_data
        # @param [Hash] options
        # @return [Hash{Symbol, Object}]
        #
        # @api private
        def parse_relationship(assoc_name, assoc_data, options)
          prefix_key = field_key(assoc_name, options).to_s.singularize
          hash =
            if assoc_data.is_a?(Array)
              { "#{prefix_key}_ids".to_sym => assoc_data.map { |ri| ri['id'] } }
            else
              { "#{prefix_key}_id".to_sym => assoc_data ? assoc_data['id'] : nil }
            end

          polymorphic = (options[:polymorphic] || []).include?(assoc_name.to_sym)
          if polymorphic
            hash["#{prefix_key}_type".to_sym] = assoc_data.present? ? assoc_data['type'].classify : nil
          end

          hash
        end

        # @api private
        def parse_relationships(relationships, options)
          transform_keys(relationships, options)
            .map { |(k, v)| parse_relationship(k, v['data'], options) }
            .reduce({}, :merge)
        end

        # @api private
        def transform_keys(hash, options)
          transform = options[:key_transform] || :underscore
          CaseTransform.send(transform, hash)
        end
      end
    end
  end
end