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