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