| 
 | 1 | +require 'jsonapi/parser'  | 
 | 2 | +require 'jsonapi/rails'  | 
 | 3 | + | 
1 | 4 | module ActiveModelSerializers  | 
2 | 5 |   module Adapter  | 
3 | 6 |     class JsonApi  | 
4 | 7 |       # NOTE(Experimental):  | 
5 | 8 |       # This is an experimental feature. Both the interface and internals could be subject  | 
6 | 9 |       # to changes.  | 
7 | 10 |       module Deserialization  | 
8 |  | -        InvalidDocument = Class.new(ArgumentError)  | 
9 |  | - | 
10 | 11 |         module_function  | 
11 | 12 | 
 
  | 
12 | 13 |         # Transform a JSON API document, containing a single data object,  | 
@@ -73,140 +74,47 @@ module Deserialization  | 
73 | 74 |         #     # }  | 
74 | 75 |         #  | 
75 | 76 |         def parse!(document, options = {})  | 
76 |  | -          parse(document, options) do |invalid_payload, reason|  | 
77 |  | -            fail InvalidDocument, "Invalid payload (#{reason}): #{invalid_payload}"  | 
 | 77 | +          parse(document, options) do |exception|  | 
 | 78 | +            fail exception  | 
78 | 79 |           end  | 
79 | 80 |         end  | 
80 | 81 | 
 
  | 
81 | 82 |         # Same as parse!, but returns an empty hash instead of raising InvalidDocument  | 
82 | 83 |         # on invalid payloads.  | 
83 | 84 |         def parse(document, options = {})  | 
84 |  | -          document = document.dup.permit!.to_h if document.is_a?(ActionController::Parameters)  | 
85 |  | - | 
86 |  | -          validate_payload(document) do |invalid_document, reason|  | 
87 |  | -            yield invalid_document, reason if block_given?  | 
88 |  | -            return {}  | 
89 |  | -          end  | 
90 |  | - | 
91 |  | -          primary_data = document['data']  | 
92 |  | -          attributes = primary_data['attributes'] || {}  | 
93 |  | -          attributes['id'] = primary_data['id'] if primary_data['id']  | 
94 |  | -          relationships = primary_data['relationships'] || {}  | 
95 |  | - | 
96 |  | -          filter_fields(attributes, options)  | 
97 |  | -          filter_fields(relationships, options)  | 
98 |  | - | 
99 |  | -          hash = {}  | 
100 |  | -          hash.merge!(parse_attributes(attributes, options))  | 
101 |  | -          hash.merge!(parse_relationships(relationships, options))  | 
102 |  | - | 
103 |  | -          hash  | 
104 |  | -        end  | 
105 |  | - | 
106 |  | -        # Checks whether a payload is compliant with the JSON API spec.  | 
107 |  | -        #  | 
108 |  | -        # @api private  | 
109 |  | -        # rubocop:disable Metrics/CyclomaticComplexity  | 
110 |  | -        def validate_payload(payload)  | 
111 |  | -          unless payload.is_a?(Hash)  | 
112 |  | -            yield payload, 'Expected hash'  | 
113 |  | -            return  | 
114 |  | -          end  | 
115 |  | - | 
116 |  | -          primary_data = payload['data']  | 
117 |  | -          unless primary_data.is_a?(Hash)  | 
118 |  | -            yield payload, { data: 'Expected hash' }  | 
119 |  | -            return  | 
120 |  | -          end  | 
121 |  | - | 
122 |  | -          attributes = primary_data['attributes'] || {}  | 
123 |  | -          unless attributes.is_a?(Hash)  | 
124 |  | -            yield payload, { data: { attributes: 'Expected hash or nil' } }  | 
125 |  | -            return  | 
126 |  | -          end  | 
127 |  | - | 
128 |  | -          relationships = primary_data['relationships'] || {}  | 
129 |  | -          unless relationships.is_a?(Hash)  | 
130 |  | -            yield payload, { data: { relationships: 'Expected hash or nil' } }  | 
131 |  | -            return  | 
132 |  | -          end  | 
133 |  | - | 
134 |  | -          relationships.each do |(key, value)|  | 
135 |  | -            unless value.is_a?(Hash) && value.key?('data')  | 
136 |  | -              yield payload, { data: { relationships: { key => 'Expected hash with :data key' } } }  | 
137 |  | -            end  | 
138 |  | -          end  | 
 | 85 | +          # TODO: change to jsonapi-ralis to have default conversion to flat hashes  | 
 | 86 | +          result = JSONAPI::Deserializable::ActiveRecord.new(document, options: options).to_hash  | 
 | 87 | +          result = apply_options(result, options)  | 
 | 88 | +          result  | 
 | 89 | +        rescue JSONAPI::Parser::InvalidDocument => e  | 
 | 90 | +          return {} unless block_given?  | 
 | 91 | +          yield e  | 
139 | 92 |         end  | 
140 |  | -        # rubocop:enable Metrics/CyclomaticComplexity  | 
141 |  | - | 
142 |  | -        # @api private  | 
143 |  | -        def filter_fields(fields, options)  | 
144 |  | -          if (only = options[:only])  | 
145 |  | -            fields.slice!(*Array(only).map(&:to_s))  | 
146 |  | -          elsif (except = options[:except])  | 
147 |  | -            fields.except!(*Array(except).map(&:to_s))  | 
148 |  | -          end  | 
149 |  | -        end  | 
150 |  | - | 
151 |  | -        # @api private  | 
152 |  | -        def field_key(field, options)  | 
153 |  | -          (options[:keys] || {}).fetch(field.to_sym, field).to_sym  | 
154 |  | -        end  | 
155 |  | - | 
156 |  | -        # @api private  | 
157 |  | -        def parse_attributes(attributes, options)  | 
158 |  | -          transform_keys(attributes, options)  | 
159 |  | -            .map { |(k, v)| { field_key(k, options) => v } }  | 
160 |  | -            .reduce({}, :merge)  | 
161 |  | -        end  | 
162 |  | - | 
163 |  | -        # Given an association name, and a relationship data attribute, build a hash  | 
164 |  | -        # mapping the corresponding ActiveRecord attribute to the corresponding value.  | 
165 |  | -        #  | 
166 |  | -        # @example  | 
167 |  | -        #   parse_relationship(:comments, [{ 'id' => '1', 'type' => 'comments' },  | 
168 |  | -        #                                  { 'id' => '2', 'type' => 'comments' }],  | 
169 |  | -        #                                 {})  | 
170 |  | -        #    # => { :comment_ids => ['1', '2'] }  | 
171 |  | -        #   parse_relationship(:author, { 'id' => '1', 'type' => 'users' }, {})  | 
172 |  | -        #    # => { :author_id => '1' }  | 
173 |  | -        #   parse_relationship(:author, nil, {})  | 
174 |  | -        #    # => { :author_id => nil }  | 
175 |  | -        # @param [Symbol] assoc_name  | 
176 |  | -        # @param [Hash] assoc_data  | 
177 |  | -        # @param [Hash] options  | 
178 |  | -        # @return [Hash{Symbol, Object}]  | 
179 |  | -        #  | 
180 |  | -        # @api private  | 
181 |  | -        def parse_relationship(assoc_name, assoc_data, options)  | 
182 |  | -          prefix_key = field_key(assoc_name, options).to_s.singularize  | 
183 |  | -          hash =  | 
184 |  | -            if assoc_data.is_a?(Array)  | 
185 |  | -              { "#{prefix_key}_ids".to_sym => assoc_data.map { |ri| ri['id'] } }  | 
186 |  | -            else  | 
187 |  | -              { "#{prefix_key}_id".to_sym => assoc_data ? assoc_data['id'] : nil }  | 
188 |  | -            end  | 
189 |  | - | 
190 |  | -          polymorphic = (options[:polymorphic] || []).include?(assoc_name.to_sym)  | 
191 |  | -          if polymorphic  | 
192 |  | -            hash["#{prefix_key}_type".to_sym] = assoc_data.present? ? assoc_data['type'] : nil  | 
193 |  | -          end  | 
194 | 93 | 
 
  | 
 | 94 | +        def apply_options(hash, options)  | 
 | 95 | +          hash = transform_keys(hash, options) if options[:key_transform]  | 
 | 96 | +          hash = hash.deep_symbolize_keys  | 
 | 97 | +          hash = rename_fields(hash, options)  | 
195 | 98 |           hash  | 
196 | 99 |         end  | 
197 | 100 | 
 
  | 
198 |  | -        # @api private  | 
199 |  | -        def parse_relationships(relationships, options)  | 
200 |  | -          transform_keys(relationships, options)  | 
201 |  | -            .map { |(k, v)| parse_relationship(k, v['data'], options) }  | 
202 |  | -            .reduce({}, :merge)  | 
203 |  | -        end  | 
204 |  | - | 
 | 101 | +        # TODO: transform the keys after parsing  | 
205 | 102 |         # @api private  | 
206 | 103 |         def transform_keys(hash, options)  | 
207 | 104 |           transform = options[:key_transform] || :underscore  | 
208 | 105 |           CaseTransform.send(transform, hash)  | 
209 | 106 |         end  | 
 | 107 | + | 
 | 108 | +        def rename_fields(hash, options)  | 
 | 109 | +          return hash unless options[:keys]  | 
 | 110 | + | 
 | 111 | +          keys = options[:keys]  | 
 | 112 | +          hash.each_with_object({}) do |(k, v), h|  | 
 | 113 | +            k = keys.fetch(k, k)  | 
 | 114 | +            h[k] = v  | 
 | 115 | +            h  | 
 | 116 | +          end  | 
 | 117 | +        end  | 
210 | 118 |       end  | 
211 | 119 |     end  | 
212 | 120 |   end  | 
 | 
0 commit comments