-
Couldn't load subscription status.
- Fork 1.4k
Utilize JSONAPI family of gems for deserialization. #1928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
10b4850
90307f0
2f99219
d6e98fb
34c7586
8ff403f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,6 @@ class JsonApi | |
| # This is an experimental feature. Both the interface and internals could be subject | ||
| # to changes. | ||
| module Deserialization | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At the end of this PR, this should be removed / deprecated /cc @richmolj - rumor has it you want to tackle moving deserialization out? |
||
| InvalidDocument = Class.new(ArgumentError) | ||
|
|
||
| module_function | ||
|
|
||
| # Transform a JSON API document, containing a single data object, | ||
|
|
@@ -73,140 +71,46 @@ module Deserialization | |
| # # } | ||
| # | ||
| def parse!(document, options = {}) | ||
| parse(document, options) do |invalid_payload, reason| | ||
| fail InvalidDocument, "Invalid payload (#{reason}): #{invalid_payload}" | ||
| parse(document, options) do |exception| | ||
| fail exception | ||
| 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) | ||
| result = JSONAPI::Deserializable::Resource.call(document) | ||
| result = apply_options(result, options) | ||
| result | ||
| rescue JSONAPI::Parser::InvalidDocument => e | ||
| return {} unless block_given? | ||
| yield e | ||
| 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'] : nil | ||
| end | ||
|
|
||
| def apply_options(hash, options) | ||
| hash = transform_keys(hash, options) if options[:key_transform] | ||
| hash = hash.deep_symbolize_keys | ||
| hash = rename_fields(hash, options) | ||
| 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 | ||
|
|
||
| # TODO: transform the keys after parsing | ||
| # @api private | ||
| def transform_keys(hash, options) | ||
| transform = options[:key_transform] || :underscore | ||
| CaseTransform.send(transform, hash) | ||
| end | ||
|
|
||
| def rename_fields(hash, options) | ||
| return hash unless options[:keys] | ||
|
|
||
| keys = options[:keys] | ||
| hash.each_with_object({}) do |(k, v), h| | ||
| k = keys.fetch(k, k) | ||
| h[k] = v | ||
| h | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I always prefer
~>here or at least< 1. Who knows if you'll give up on AMS and break something with a 1.0 release some day :)