Skip to content
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

Utilize JSONAPI family of gems for deserialization. #1928

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion active_model_serializers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ Gem::Specification.new do |spec|
# 'minitest'
# 'thread_safe'

spec.add_runtime_dependency 'jsonapi', '0.1.1.beta2'
spec.add_runtime_dependency 'jsonapi-deserializable', '~> 0.1.1'
spec.add_runtime_dependency 'jsonapi-renderer', '~> 0.1.1'
spec.add_runtime_dependency 'case_transform', '>= 0.2'
Copy link
Contributor

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


spec.add_development_dependency 'activerecord', rails_versions
Expand Down
1 change: 1 addition & 0 deletions lib/active_model_serializers.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'active_model'
require 'active_support'
require 'jsonapi/deserializable'
require 'active_support/core_ext/object/with_options'
require 'active_support/core_ext/string/inflections'
require 'active_support/json'
Expand Down
144 changes: 24 additions & 120 deletions lib/active_model_serializers/adapter/json_api/deserialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ class JsonApi
# This is an experimental feature. Both the interface and internals could be subject
# to changes.
module Deserialization
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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
Expand Down
112 changes: 0 additions & 112 deletions test/action_controller/json_api/deserialization_test.rb

This file was deleted.

Loading