Skip to content

Commit

Permalink
begin playing with using JSON API for deserialization
Browse files Browse the repository at this point in the history
extract key_transfor

chaneg to case_transform gem

fix

tests pass

add empty array test

upgrade to jsonapi beta5, improve tests to be valid resource requests

use github links in gemfile for now, due to unreleased code

use github links in gemfile for now, due to unreleased code

some clenaup

deserialization would work now if it didn't depend on the existance of certain objects

progress on implementing new deserializer -- just have some behavior differences with relationships

tests pass

address rubocop issue -- update dependencies... wonder if we need to move @beauby's jsonapi gems to rails-api, in order to faster turnaround time

reduce to minimum dependencies. Now we need beauby to merge my jsonapi-rails branch :-)
  • Loading branch information
NullVoxPopuli committed Dec 7, 2016
1 parent 0422a1e commit 10b4850
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 373 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ eval_gemfile local_gemfile if File.readable?(local_gemfile)
# Specify your gem's dependencies in active_model_serializers.gemspec
gemspec

gem 'jsonapi-rails', github: 'beauby/jsonapi-rails', branch: 'initial-implementation'

version = ENV['RAILS_VERSION'] || '4.2'

if version == 'master'
Expand Down
2 changes: 1 addition & 1 deletion active_model_serializers.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ 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.beta3'
spec.add_runtime_dependency 'case_transform', '>= 0.2'

spec.add_development_dependency 'activerecord', rails_versions
Expand Down
148 changes: 28 additions & 120 deletions lib/active_model_serializers/adapter/json_api/deserialization.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
require 'jsonapi/parser'
require 'jsonapi/rails'

module ActiveModelSerializers
module Adapter
class JsonApi
# NOTE(Experimental):
# This is an experimental feature. Both the interface and internals could be subject
# to changes.
module Deserialization
InvalidDocument = Class.new(ArgumentError)

module_function

# Transform a JSON API document, containing a single data object,
Expand Down Expand Up @@ -73,140 +74,47 @@ 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
# TODO: change to jsonapi-ralis to have default conversion to flat hashes
result = JSONAPI::Deserializable::ActiveRecord.new(document, options: options).to_hash
result = apply_options(result, options)
result
rescue JSONAPI::Parser::InvalidDocument => e
return {} unless block_given?
yield e
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)
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.

6 changes: 3 additions & 3 deletions test/action_controller/json_api/transform_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
module ActionController
module Serialization
class JsonApi
class KeyTransformTest < ActionController::TestCase
class KeyTransformTestController < ActionController::Base
class CaseTransformTest < ActionController::TestCase
class CaseTransformTestController < ActionController::Base
class Post < ::Model
attributes :title, :body, :publish_at
associations :author, :top_comments
Expand Down Expand Up @@ -77,7 +77,7 @@ def render_resource_with_transform_with_global_config
end
end

tests KeyTransformTestController
tests CaseTransformTestController

def test_render_resource_with_transform
get :render_resource_with_transform
Expand Down
Loading

0 comments on commit 10b4850

Please sign in to comment.