|  | 
|  | 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