Apply method to all attributes using active_model_serializers - ruby-on-rails

Using rails-api/active_model_serializers, could one apply a method to all attributes?
Basically, I wish to apply
object.zeroed_value(:symbol)
to each attribute without having to write a separate method for each. See example:
class NutritionalSerializer < ActiveModel::Serializer
attributes :calories,
:sodium
def calories
object.zeroed_value(:calories)
end
def sodium
object.zeroed_value(:sodium)
end
# many, many more attributes...
end

I think you can use metaprogramming to generate all methods you need with something like:
class NutritionalSerializer < ActiveModel::Serializer
attributes :calories,
:sodium
%i{attr1 attr2 attr3}.each do |attr|
define_method attr do
object.zeroed_value(attr)
end
end
end
Also you can override the attributes method of serializer and then do something like:
class NutritionalSerializer < ActiveModel::Serializer
def attributes
data = super
%i{attr1 attr2 attr3}.each do |attr|
data[attr] = object.zeroed_value(attr)
end
data
end
end

Related

Ruby on Rails 5 - Passing data between two serializers

I would like to know if it is possible to send data from a serializer to another, not from a controller to a serializer. Here is what I am doing :
class Serializer1 < ActiveModel::Serializer
attributes \
:id,
:past_teachings
def past_teachings
p_teachings = Teaching.all
p_teachings = ActiveModel::ArraySerializer.new(p_teachings,
each_serializer: Serializer2)
#### I would like to send data to serializer2 from the current serializer ####
end
end
I know it is possible to send data from a controller to a serializer. But it is possible to send data from a serializer to another?
Yes. Using AMS 0.10.x, you could change your example in this way:
serializer_1.rb
class Serializer1 < ActiveModel::Serializer
attributes :id, :past_teachings
def past_teachings
ActiveModelSerializers::SerializableResource.new(PastTeaching.all, each_serializer: TeachingSerializer)
end
end
teaching_serializer.rb:
class TeachingSerializer < ActiveModel::Serializer
attributes :id, :name
end
If you want to access the current object being serialized, you can refer to 'object.' You can access the objects functions as object.function and its attributes as object['attribute'].
So, technically, you could do something like this (though in reality you would probably use AMS has_many relationship instead):
class Serializer1 < ActiveModel::Serializer
attributes :id, :past_teachings
def past_teachings
ActiveModelSerializers::SerializableResource.new(object.past_teachings.where(...), each_serializer: TeachingSerializer)
end
end

How to return all attributes of an object with Rails Serializer?

I have a simple question. I have a seriaizer that looks like this:
class GroupSerializer < ActiveModel::Serializer
attributes :id, :name, :about, :city
end
The problem is that, whenever I change my model, I have to add/remove attributes from this serializer. I just want to get the whole object by default as in the default rails json respond:
render json: #group
How can I do that?
At least on 0.8.2 of ActiveModelSerializers you can use the following:
class GroupSerializer < ActiveModel::Serializer
def attributes
object.attributes.symbolize_keys
end
end
Be carful with this though as it will add every attribute that your object has attached to it. You probably will want to put in some filtering logic on your serializer to prevent sensitive information from being shown (i.e., encrypted passwords, etc...)
This does not address associations, although with a little digging around you could probably implement something similar.
============================================================
UPDATE: 01/12/2016
On 0.10.x version of ActiveModelSerializers, attributes receives two arguments by default. I added *args to avoid exception:
class GroupSerializer < ActiveModel::Serializer
def attributes(*args)
object.attributes.symbolize_keys
end
end
Just to add to #kevin's answer. I was looking also to how to add filters on the returned attributes. I looked to the the documentation active_model_serializers 0.9 and it does support filters that looks like this:
def attributes
object.attributes.symbolize_keys
end
def filter(keys)
keys - [:author, :id]
end
I tried it, but it did not work. I assumed that's because the attributes are not specified explicitly. I had to do it the same way specified in the rails cast to work:
##except=[:author, :id]
def attributes
data = object.attributes.symbolize_keys
##except.each { |e| data.delete e }
data
end
Try the following to get all the attribute keys for the Group class:
Group.new.attributes.keys
For example, I get the following for users on one app:
> User.new.attributes.keys
=> ["id", "password_digest", "auth_token", "password_reset_token", "password_reset_requested_at", "created_at", "updated_at"]
On 0.10.x version of ActiveModelSerializers, attributes receives two arguments by default. I added *args to avoid exception:
class GroupSerializer < ActiveModel::Serializer
def attributes(*args)
object.attributes.symbolize_keys
end
end
I want get all attributes + few more.
base on answer above, this work:
class NotificationSerializer < ActiveModel::Serializer
def actor
'asdasd'
end
def attributes(*args)
keys = object.attributes
keys[:actor] = actor() # add attribute here
keys.symbolize_keys
end
end

Access attributes inside hashed column

I have the following class:
class Profile < ActiveRecord::Base
serialize :data
end
Profile has a single column data that holds a serialized hash. I would like to define accessors into that hash such that I can execute profile.name instead of profile.data['name']. Is that possible in Rails?
The simple straightforward way:
class Profile < ActiveRecord::Base
serialize :data
def name
self.data['name']
end
def some_other_attribute
self.data['some_other_attribute']
end
end
You can see how that can quickly become cumbersome if you have lots of attributes within the data hash that you want to access.
So here's a more dynamic way to do it and it would work for any such top level attribute you want to access within data:
class Profile < ActiveRecord::Base
serialize :data
def method_missing(attribute, *args, &block)
return super unless self.data.key? attribute
self.data.fetch(attribute)
end
# good practice to extend respond_to? when using method_missing
def respond_to?(attribute, include_private = false)
super || self.data.key?(attribute)
end
end
With the latter approach you can just define method_missing and then call any attribute on #profile that is a key within data. So calling #profile.name would go through method_missing and grab the value from self.data['name']. This will work for whatever keys are present in self.data. Hope that helps.
Further reading:
http://www.trottercashion.com/2011/02/08/rubys-define_method-method_missing-and-instance_eval.html
http://technicalpickles.com/posts/using-method_missing-and-respond_to-to-create-dynamic-methods/
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
def name
data[:name] if data
end
end
I'm going to answer my own question. It looks like ActiveRecord::Store is what I want:
http://api.rubyonrails.org/classes/ActiveRecord/Store.html
So my class would become:
class Profile < ActiveRecord::Base
store :data, accessors: [:name], coder: JSON
end
I'm sure everyone else's solutions work just fine, but this is so clean.
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
["name", "attr2", "attr3"].each do |method|
define_method(method) do
data[method.to_sym] if data
end
end
end
Ruby is extremely flexible and your model is just a Ruby Class. Define the "accessor" method you want and the output you desire.
class Profile < ActiveRecord::Base
serialize :data
def name
data['name'] if data
end
end
However, that approach is going to lead to a lot of repeated code. Ruby's metaprogramming features can help you solve that problem.
If every profile contains the same data structure you can use define_method
[:name, :age, :location, :email].each do |method|
define_method method do
data[method] if data
end
end
If the profile contains unique information you can use method_missing to attempt to look into the hash.
def method_missing(method, *args, &block)
if data && data.has_key?(method)
data[method]
else
super
end
end

Filtering fields from ActiveRecord/ActiveModel JSON output (by magic!)

I want to filter out specific fields from ActiveRecord/ActiveModel classes when outputting JSON.
The most straightforward way to do this is just overriding as_json, perhaps like so:
def as_json (options = nil)
options ||= {}
super(options.deep_merge({:except => filter_attributes}))
end
def filter_attributes
[:password_digest, :some_attribute]
end
This works, but it's a little verbose and lends itself to not being DRY pretty fast. I thought it would be nice to just declare the filtered properties with a magical class method. For example:
class User < ActiveRecord::Base
include FilterJson
has_secure_password
filter_json :password_digest
#...
end
module FilterJson
extend ActiveSupport::Concern
module ClassMethods
def filter_json (*attributes)
(#filter_attributes ||= Set.new).merge(attributes.map(&:to_s))
end
def filter_attributes
#filter_attributes
end
end
def as_json (options = nil)
options ||= {}
super(options.deep_merge({:except => self.class.filter_attributes.to_a}))
end
end
The problem with this is getting it to deal with inheritance properly. Let's say I subclass User:
class SecretiveUser < User
filter_json :some_attribute, :another_attribute
#...
end
Logically, it makes sense to filter out :some_attribute, :another_attribute, and also :password_digest.
However, this will only filter the attributes declared on the class. To the desired end, I tried to call super within filter_attributes, but that failed. I came up with this, and it's a hack.
def filter_attributes
if superclass.respond_to?(:filter_attributes)
superclass.filter_attributes + #filter_attributes
else
#filter_attributes
end
end
This is obviously brittle and not idiomatic, but there's the "what" that I'm trying to accomplish.
Can anyone think of a way to do it more correctly (and hopefully more elegantly)? Thanks!
I think it is a safer solution to white-list attributes than to black-list them. This will prevent unwanted future attributes added to User or SomeUser from making it into your JSON response because you forgot to add said attributes to filter_json.
You seem to be looking for a solution to your specific inheritance issue. I'm still going to point out active_model_serializers, as I feel it is a saner way to manage serialization.
class UserSerializer < ActiveModel::Serializer
attributes :id, :first_name, :last_name
end
class SecretUserSerializer < UserSerializer
attributes :secret_attribute, :another_attribute
end
Given some SecretUser s you can do
SecretUserSerializer.new(s).as_json
and you'll get :id, :first_name, :last_name, :secret_attribute, and :another_attribute. The inheritance works as expected.

How to override default deserialization of params to model object?

How to override default deserialization of params to model object?
In other words, how to make Rails understand camel case JSON with a snake case database?
Example: I receive params Foo object with a field fooBar and I want my Foo model to understand fooBar is in fact database field foo_bar.
"Foo": {
"fooBar": "hello" /* fooBar is database field foo_bar */
}
class Foo < ActiveRecord::Base
attr_accessible :foo_bar
end
class FoosController < ApplicationController
def new
#foo = Foo.new(params[:foo])
end
Foo.new(params[:foo]) assumes params[:foo] contains foo_bar. Instead params[:foo] contains fooBar (in my case params contains JSON data).
I would like a clean way to handle this case, the same way a model can override as_json:
class Foo < ActiveRecord::Base
attr_accessible :foo_bar, :another_field
def as_json(options = nil)
{
fooBar: foo_bar,
anotherField: another_field
}
end
end
There is a from_json method inside ActiveModel but it is not called when Foo.new(params[:foo]) is run.
I've read several times that overriding initialize from a model object is a terrible idea.
All that Foo.new does with the params hash you give it is iterate over the keys and values in that hash. If the key is foo_bar then it tries to call foo_bar= with the value.
If you define a fooBar= method that sets self.foo_bar then you'll be able to pass a hash with the key :fooBar to Foo.new.
Less manually, you can do
class Foo < ActiveRecord::Base
alias_attribute :fooBar, :foo_bar
end
which generates all the extra accessors for you.
I wouldn't say that overriding initialize is a terrible thing but it can be tricky to do right and there's almost always a simpler way or a way that makes your intentions clearer.
I've checked active_model_serializers, RABL and JBuilder. None of them allow to customize the JSON format that is received.
For that one must deal with wrap_parameters, see http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html
It works, still the code is ugly: I get JSON stuff inside my controller + the serializer/model instead of one place.
Example of use of wrap_parameters:
class EventsController < ApplicationController
wrap_parameters :event, include: [:title, :start, :end, :allDay, :description, :location, :color]
def create
respond_with Event.create(params[:event])
end
end
and then inside my model (Frederick Cheung is right on this part):
class Event < ActiveRecord::Base
attr_accessible :title, :start, :end, :allDay, :description, :location, :color
# JSON input allDay is all_day
alias_attribute :allDay, :all_day
# JSON input start is starts_at
# +datetime+:: UNIX time
def start=(datetime)
self.starts_at = Time.at(datetime)
end
# JSON input end is starts_at
# +datetime+:: UNIX time
def end=(datetime)
self.ends_at = Time.at(datetime)
end
# Override the JSON that is returned
def as_json(options = nil)
{
id: id,
title: title,
start: starts_at, # ISO 8601, ex: "2011-10-28T01:22:00Z"
end: ends_at,
allDay: all_day,
description: description, # Not rendered by FullCalendar
location: location,
color: color
}
end
end
For info ASP.NET MVC (with Json.NET) does it using C# decorator attributes which is pretty elegant:
class Post
{
[JsonPropertyAttribute("title")]
public string Title;
}
I have created a gist that shows how to implement serialization/deserialization: https://gist.github.com/3858908

Resources