Skip to content

WIP - Deserializer implementation #950

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
2a3c0a5
starting deserializer architecture
joaomdmoura Jun 11, 2015
71b8419
requiring deserializer
joaomdmoura Jun 11, 2015
4b490e4
removing useless autoload for now
joaomdmoura Jun 11, 2015
f082e82
Adding initial implementation, including wrap_parameters
joaomdmoura Jun 11, 2015
fb08977
starting to update readme
joaomdmoura Jun 11, 2015
e700a29
starting deserializer parsing based on serializer defined attributes
joaomdmoura Jun 11, 2015
c081b60
updating README to support the new implementation
joaomdmoura Jun 29, 2015
64dd2ed
updating deserialization implementation to not use module but a new s…
joaomdmoura Jun 29, 2015
ed856d3
updating generator and root_name parsing
joaomdmoura Jul 2, 2015
2bd9687
updating tests to work with the new Serialisation convention
joaomdmoura Jul 2, 2015
dd18f7f
partially updating README
joaomdmoura Jul 2, 2015
1a124c2
adding attribute as default params value
joaomdmoura Jul 4, 2015
387f2a5
implementing strong parameters logic to work with adapters
joaomdmoura Jul 4, 2015
a2e91f2
updating code formatting and conventions
joaomdmoura Jul 5, 2015
8a63c85
updating permit and deserialize method
joaomdmoura Jul 5, 2015
ce1534d
starting adapter update to support deserialization
joaomdmoura Jul 5, 2015
622a1ce
updatign some tests after rebasing with master
joaomdmoura Jul 8, 2015
101cd1e
TYPO
joaomdmoura Jul 12, 2015
4090a96
starting adapters param's parser
joaomdmoura Jul 12, 2015
260cafd
renaming serializer files in order to fix tests
Jul 22, 2015
1c207dd
refactoring deserialization implementation to focus on permit attributes
Jul 23, 2015
1abd56c
adding support to relationships
Jul 23, 2015
e108572
starting deserialization integration tests structure
Jul 26, 2015
64e810d
addnig first json api deserialization test
Jul 27, 2015
ec88329
rebasing with master and fixing conflits to make it work
Aug 3, 2015
f8d2098
adding singularize when parsing relationships
Aug 21, 2015
f1524f0
Add relationship information to PORO models.
beauby Aug 21, 2015
489e317
Reorganize code.
beauby Aug 21, 2015
7ad34f0
updating new json-api test and related structure
Aug 21, 2015
5bc262f
rolling back serializer -> serialization name change
Aug 21, 2015
6f2acde
Clean rewrite of deserializers, following the various points discussed.
beauby Aug 21, 2015
38f9b03
Add tests for the parsing part of deserialization.
beauby Aug 21, 2015
43ca7e8
Rename misleading method params into params_whitelist.
beauby Aug 21, 2015
55a0bd1
Allow scalar values as resource identifiers in order to handle nil.
beauby Aug 21, 2015
83713d2
Fix @_params never getting set after initialization.
beauby Aug 21, 2015
529508f
Add tests for deserializers' sanitize_params.
beauby Aug 21, 2015
6a3d7a5
Make style consistent.
beauby Aug 21, 2015
2e1e6ae
Revert "Reorganize code."
beauby Aug 21, 2015
042ac69
Revert "Add relationship information to PORO models."
beauby Aug 21, 2015
41bfb36
Clean up.
beauby Aug 21, 2015
e4c571c
Add whitelist to attributes/relationships + tests.
beauby Aug 21, 2015
1223b43
Handle id in whitelist.
beauby Aug 21, 2015
3c0c68b
Add test for deserialization into actual ActiveRecord model.
beauby Aug 21, 2015
c307ed5
Clean up.
beauby Aug 21, 2015
5f36bf0
Fix Gemfile for jruby.
beauby Aug 22, 2015
c29de5a
adding back params method after rebase
Aug 26, 2015
7448e2a
removing unecessary test
Aug 26, 2015
c754a19
Fix previous merge.
beauby Aug 26, 2015
c8d2fc1
Merge pull request #7 from beauby/joao-deserializer2
joaomdmoura Aug 26, 2015
7691298
Namespace AR models + integration test.
beauby Aug 26, 2015
89902b5
Fix extra spaces.
beauby Aug 26, 2015
d1aaa37
Merge pull request #9 from beauby/joao-deserializer4
joaomdmoura Aug 26, 2015
169424f
Minor code improvement.
beauby Aug 26, 2015
4cb08e3
Merge pull request #10 from beauby/joao-deserializer5
joaomdmoura Aug 26, 2015
0d2ef9a
Revert Serialization renaming.
beauby Aug 26, 2015
48412a6
Merge pull request #11 from beauby/joao-deserializer6
joaomdmoura Aug 28, 2015
cd48fca
Merge pull request #8 from beauby/joao-deserializer3
joaomdmoura Aug 28, 2015
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
6 changes: 5 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in active_model_serializers.gemspec
gemspec

gem "minitest"
group :development do
gem "minitest"
gem "sqlite3", platform: :ruby
gem "activerecord-jdbcsqlite3-adapter", platform: :jruby
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These being test dependencies should be in the gemspec, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would agree, but since "minitest" was there already, I did as the Romans do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did as the Romans do.

😂
Yup.


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

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
ActiveModel::Serializer brings convention over configuration to your JSON generation.

AMS does this through two components: **serializers** and **adapters**.
Serializers describe _which_ attributes and relationships should be serialized.
Adapters describe _how_ attributes and relationships should be serialized.
Serializers describe _which_ attributes and relationships should be serialized and deserialized.
Adapters describe _how_ attributes and relationships should be serialized and deserialized.

By default AMS will use the **Flatten Json Adapter**. But we strongly advise you to use **JsonApi Adapter** that follows 1.0 of the format specified in [jsonapi.org/format](http://jsonapi.org/format).
Check how to change the adapter in the sections bellow.
Expand Down
33 changes: 32 additions & 1 deletion lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Serializer

class << self
attr_accessor :_attributes
attr_accessor :_params
attr_accessor :_attributes_keys
attr_accessor :_urls
attr_accessor :_cache
Expand All @@ -28,16 +29,24 @@ class << self
def self.inherited(base)
base._attributes = self._attributes.try(:dup) || []
base._attributes_keys = self._attributes_keys.try(:dup) || {}
base._params = self._attributes.try(:dup) || []
base._urls = []

serializer_file = File.open(caller.first[/^[^:]+/])
base._cache_digest = Digest::MD5.hexdigest(serializer_file.read)
super
end

def self.params(*attrs)
@_params.concat attrs
@_params.uniq!
end

def self.attributes(*attrs)
attrs = attrs.first if attrs.first.class == Array
@_attributes.concat attrs
@_attributes.uniq!
@_params = @_attributes

attrs.each do |attr|
define_method attr do
Expand All @@ -50,6 +59,7 @@ def self.attribute(attr, options = {})
key = options.fetch(:key, attr)
@_attributes_keys[attr] = { key: key } if key != attr
@_attributes << key unless @_attributes.include?(key)
@_params << key unless @_params.include?(key)

unless respond_to?(key, false) || _fragmented.respond_to?(attr)
define_method key do
Expand Down Expand Up @@ -105,7 +115,28 @@ def self.adapter
end

def self.root_name
name.demodulize.underscore.sub(/_serializer$/, '') if name
name.demodulize.underscore.sub(/_serialization$/, '') if name
end

def self.sanitize_params(params, whitelist = nil)
attrs = @_params
assocs = _reflections.map(&:name)
forbid_id = false
if whitelist
assocs &= whitelist
attrs &= whitelist
forbid_id = !whitelist.include?(:id)
end
permitted_params = adapter.params_whitelist(attrs, assocs, forbid_id)
permitted_key = adapter.root || root_name

params.require(permitted_key.to_sym).permit(permitted_params)
end

def self.deserialize(params, whitelist = [])
whitelist.map! { |x| x.to_sym }
sanitized_params = sanitize_params(params, whitelist)
adapter.parse(sanitized_params)
end

attr_accessor :object, :root, :meta, :meta_key, :scope
Expand Down
5 changes: 5 additions & 0 deletions lib/active_model/serializer/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ class Adapter
autoload :Null
autoload :JsonApi

class << self
attr_accessor :root
attr_accessor :params
end

attr_reader :serializer

def initialize(serializer, options = {})
Expand Down
55 changes: 47 additions & 8 deletions lib/active_model/serializer/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,47 @@ module ActiveModel
class Serializer
class Adapter
class JsonApi < Adapter
@root = :data
DEFAULT_ATTRIBUTES = [:id, :type]

def self.params_whitelist(permitted, associations, forbid_id = false)
relationships = {}
associations.each do |assoc|
relationships[assoc] = [ @root, @root => DEFAULT_ATTRIBUTES ]
end
whitelist = :type, { attributes: permitted }, { relationships: relationships }
whitelist << :id unless forbid_id

whitelist
end

def self.parse(params)
attrs, assoc = {}, {}
attrs = params['attributes'] if params['attributes']
attrs['id'] = params['id'] if params['id']
assoc = params['relationships'].map do |rel|
key, data = rel.shift.singularize, rel.first['data']
key = if data.kind_of? Array
"#{key}_ids"
else
"#{key}_id"
end
value = if data.kind_of? Array
data.map { |ri| ri['id'] }
elsif data
data['id']
else
nil
end
{key => value}
end if params['relationships']
assoc.reduce attrs, :merge
end

def initialize(serializer, options = {})
super
@hash = { data: [] }
@root = self.class.root
@hash = { @root => [] }

if fields = options.delete(:fields)
@fieldset = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key)
Expand All @@ -21,7 +59,7 @@ def serializable_hash(options = nil)
if serializer.respond_to?(:each)
serializer.each do |s|
result = self.class.new(s, @options.merge(fieldset: @fieldset)).serializable_hash(options)
@hash[:data] << result[:data]
@hash[@root] << result[@root]

if result[:included]
@hash[:included] ||= []
Expand All @@ -31,8 +69,8 @@ def serializable_hash(options = nil)

add_links(options)
else
@hash[:data] = attributes_for_serializer(serializer, options)
add_resource_relationships(@hash[:data], serializer)
@hash[@root] = attributes_for_serializer(serializer, options)
add_resource_relationships(@hash[@root], serializer)
end
@hash
end
Expand All @@ -46,16 +84,17 @@ def fragment_cache(cached_hash, non_cached_hash)

def add_relationships(resource, name, serializers)
resource[:relationships] ||= {}
resource[:relationships][name] ||= { data: [] }
resource[:relationships][name][:data] += serializers.map { |serializer| { type: serializer.json_api_type, id: serializer.id.to_s } }
resource[:relationships][name] ||= { @root => [] }
resource[:relationships][name][@root] ||= []
resource[:relationships][name][@root] += serializers.map { |serializer| { type: serializer.json_api_type, id: serializer.id.to_s } }
end

def add_relationship(resource, name, serializer, val=nil)
resource[:relationships] ||= {}
resource[:relationships][name] = { data: val }
resource[:relationships][name] = { @root => val }

if serializer && serializer.object
resource[:relationships][name][:data] = { type: serializer.json_api_type, id: serializer.id.to_s }
resource[:relationships][name][@root] = { type: serializer.json_api_type, id: serializer.id.to_s }
end
end

Expand Down
76 changes: 76 additions & 0 deletions test/action_controller/deserialization_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require 'test_helper'

module ActionController
module Serializer
class ImplicitDeserializerTest < ActionController::TestCase
class ImplicitDeserializerTestController < ActionController::Base
def create_resource
with_adapter :json_api do
@post = ARModels::Post.create(create_params)
render json: @post, status: :created
end
end

private

def create_params
ARModels::PostSerializer.deserialize(params)
end

def with_adapter(adapter)
old_adapter = ActiveModel::Serializer.config.adapter
# JSON-API adapter sets root by default
ActiveModel::Serializer.config.adapter = adapter
yield
ensure
ActiveModel::Serializer.config.adapter = old_adapter
end
end

tests ImplicitDeserializerTestController

def test_json_api_deserialization_on_create
payload = {
data: {
type: 'posts',
attributes: {
title: 'Title 1',
body: 'Body 1'
}
}
}

post :create_resource, payload
new_post = JSON.parse(@response.body)

assert_equal payload[:data][:attributes][:title], new_post['data']['attributes']['title']
assert_equal payload[:data][:attributes][:body], new_post['data']['attributes']['body']
end

def test_json_api_deserialization_on_create_with_associations
comment = ARModels::Comment.create(contents: "Comment 1")
payload = {
data: {
type: 'posts',
attributes: {
title: 'Title 1',
body: 'Body 1'
},
relationships: {
comments: {
data: [{ id: comment.id, type: 'comments' }]
}
}
}
}

post :create_resource, payload
new_post = JSON.parse(@response.body)

assert_equal 1, new_post['data']['relationships']['comments']['data'].length
assert_equal "#{comment.id}", new_post['data']['relationships']['comments']['data'][0]['id']
assert_equal 'ar_models_comments', new_post['data']['relationships']['comments']['data'][0]['type']
end
end
end
end
2 changes: 1 addition & 1 deletion test/action_controller/serialization_scope_name_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ def test_override_scope_name_with_controller
get :render_new_user
assert_equal '{"data":{"id":"1","type":"users","attributes":{"admin?":true}}}', @response.body
end
end
end
1 change: 0 additions & 1 deletion test/adapter/fragment_cache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,3 @@ def test_fragment_fetch_with_namespaced_object
end
end
end

49 changes: 49 additions & 0 deletions test/deserializers/deserialize_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require 'test_helper'

module ActiveModel
class Serializer
class DeserializeTest < Minitest::Test
def with_adapter(adapter)
old_adapter = ActiveModel::Serializer.config.adapter
# JSON-API adapter sets root by default
ActiveModel::Serializer.config.adapter = adapter
yield
ensure
ActiveModel::Serializer.config.adapter = old_adapter
end

def test_json_api_deserialize_on_create
author = ARModels::Author.create(name: "Author 1")
comment1 = ARModels::Comment.create(contents: "Comment 1", author: author)
comment2 = ARModels::Comment.create(contents: "Comment 2", author: author)
payload = {
data: {
type: 'posts',
attributes: {
title: 'Title 1',
body: 'Body 1'
},
relationships: {
author: {
data: { id: author.id, type: 'authors'}
},
comments: {
data: [{ id: comment1.id, type: 'comments'},
{ id: comment2.id, type: 'comments'}]
}
}
}
}

post = with_adapter :json_api do
ARModels::Post.create(ARModels::PostSerializer.deserialize(ActionController::Parameters.new(payload)))
end

assert_equal('Title 1', post.title)
assert_equal('Body 1', post.body)
assert_equal(2, post.comments.count)
assert_equal('Author 1', post.author.name)
end
end
end
end
Loading