Conditional attributes and methods in rails serializer 0.10 - ruby-on-rails

class Api::V1::BookSerializer < ActiveModel::Serializer
attributes :id, :status, :name, :author_name, :published_date
attributes :conditional_attributes if condition_1?
belongs_to :user if condition_2?
end
Here I want to put condition on action basic of the controller.
For example I will like to send conditional_attributes for only index action and not for other actions.
But rails "active_model_serializers", "~> 0.10.0" does not give any such things according to my knowledge.

Something like this should do the trick:
class Api::V1::BookSerializer < ActiveModel::Serializer
attributes :id, :status, :name, :author_name, :published_date
attribute :conditional_attribute, if: :some_condition?
belongs_to :conditional_association, if: :some_other_condition?
private
def some_condition?
# some condition
end
def some_other_condition?
# some other condition
end
end
You can also use :unless for negated conditions.
You can use instance_options or instance_reflections in your conditions if you need them (see https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/howto/passing_arbitrary_options.md) or you can use scopes (see https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/general/serializers.md#scope)
Note: To the best of my knowledge, this only works with attribute and association methods – it doesn't work with attributes (see https://github.com/rails-api/active_model_serializers/blob/0-10-stable/lib/active_model/serializer.rb#L204-L210) since it doesn't pass options along.
I read your comment regarding sticking with AM Serializers, but I'll still point it out: If you're looking for a more robust and flexible solution than AM Serializers, jsonapi-serializer or Blueprinter work quite well and both have support for conditional fields as well as conditional associations.

I assume you're trying to render from the controller.
You can pass options to your serializer from the call to render:
render json: #track, serializer: Api::V1::BookSerializer, return_user: return_user?, return_extra_attributes: return_extra_attributes?
You can then access that option in your serializer definition, via #instance_options[:your_option].
Here, you would likely have something like:
class Api::V1::BookSerializer < ActiveModel::Serializer
attributes :id, :status, :name, :author_name, :published_date
attributes :conditional_attributes if return_conditional_attributes?
belongs_to :user if return_user?
def return_conditional_attributes?
#instance_options[:return_extra_attributes]
end
def return_user?
#instance_options[:return_user]
end
end
return_extra_attributes? and return_extra_attributes? would be method defined in your controller
documentation here: https://github.com/rails-api/active_model_serializers/blob/0-10-stable/docs/howto/passing_arbitrary_options.md

Related

Passing options to ActiveModel serializer

When using the serializer from the controller I can pass extra options to it like so
render json: user, some_option: 'foobar
Then I can reference some_option within the serializer as
serialization_options[:some_option]
But, if I call the serializer directly as
MySerializer.new(user, some_option: 'foobar')
I cannot get the extra options since serialization_options is an empty object.
For v0.9
You may call the following:
MySerializer.new(user).as_json({some_option: 'foobar'})
If you are doing that inside another serializer and you need to pass the scope and the current serialization_options as well, you can do this:
class MyParentSerializer
has_one :user
def user
MySerializer.new(object.user, { scope: scope }).as_json(serialization_options.merge({ some_option: 'foobar' }))
end
end
ActiveModel::Serializer's API has not really been consistent, in v0.9, however if you upgrade to v0.10, you could use the instance_options method to access the additional params. However, I'd be curious to learn how the objects were parsed in v0.9, though
Here is how you can pass parameters (options) from the parent serializer and show or hide attributes based on these parameters in the child serializer.
Parent serializer:
class LocationSharesSerializer < ActiveModel::Serializer
attributes :id, :locations, :show_title, :show_address
def locations
ActiveModelSerializers::SerializableResource.new(object.locations, {
each_serializer: PublicLocationSerializer,
params: {
show_title: object.show_title
},
})
end
end
Child serializer
class PublicLocationSerializer < ActiveModel::Serializer
attributes :id, :latitude, :longitude, :title, :directions, :description, :address, :tags, :created_at, :updated_at, :photos
def title
object.title if #instance_options[:params][:show_title]
end
end

how to conditionally include associations in a Rails Active Model Serializer v0.8

I have used AMS (0.8) with Rails 3.2.19 but one place where I really struggle with them is how to control whether serializers include their associations or not. I obviously use AMS to build JSON
Api's. Sometimes a serializer is the leaf or furthest out element and sometimes it's the top level and needs to include associations. My question is what is the best way to do this or is the solution I do below work (or is best solution).
I have seen some of the discussions but I find them very confusing (and version based). It's clear that for Serializer attributes or associations, there is an an include_XXX? method for each and you can return either a truthy or falsey statement here.
Here's my proposed code - it's a winemaker that has many wine_items. Is this how you would do this?
Model Classes:
class WineItem < ActiveRecord::Base
attr_accessible :name, :winemaker_id
belongs_to :winemaker
end
class Winemaker < ActiveRecord::Base
attr_accessible :name
has_many :wine_items
attr_accessor :show_items
end
Serializers:
class WinemakerSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :wine_items
def include_wine_items?
object.show_items
end
end
class WineItemSerializer < ActiveModel::Serializer
attributes :id, :name
end
and in my controller:
class ApiWinemakersController < ApplicationController
def index
#winemakers=Winemaker.all
#winemakers.each { |wm| wm.show_items=true }
render json: #winemakers, each_serializer: WinemakerSerializer, root: "data"
end
end
I ran into this issue myself and this is the cleanest solution so far (but I'm not a fan of it).
This method allows you to do things like:
/parents/1?include_children=true
or using a cleaner syntax like:
/parents/1?include=[children], etc...
# app/controllers/application_controller.rb
class ApplicationController
# Override scope for ActiveModel-Serializer (method defined below)
# See: https://github.com/rails-api/active_model_serializers/tree/0-8-stable#customizing-scope
serialization_scope(:serializer_scope)
private
# Whatever is in this method is accessible in the serializer classes.
# Pass in params for conditional includes.
def serializer_scope
OpenStruct.new(params: params, current_user: current_user)
end
end
# app/serializers/parent_serializer.rb
class ParentSerializer < ActiveModel::Serializer
has_many :children
def include_children?
params[:include_children] == true
# or if using other syntax:
# params[:includes].include?("children")
end
end
Kinda hackish to me, but it works. Hope you find it useful!

Virtual attribute in a Rails app - why is attr_writer not making my input work with my getter and setter methods?

I'm trying to roll my own tagging system. My setup is (at the moment) much like acts_as_taggable_on, with Tags, Taggable objects, and Taggings to relate the one to the other. Taggable is a module, which will be included in Events, Users, and probably a few other kinds of objects that will be taggable. At the moment I'm just trying to hook it up to work with Events.
I'm following Railscast #167.
In the railscast, the virtual attribute tag_names is made accessible with attr_writer :tag_names.
My problem is, I can't get the tag_names field to accept input unless I use attr_accessible :tag_names (ie, 'attr_accessible' instead of 'attr_writer').
when specifying attr_writer :tag_names, I submit the form and get the error: "Can't mass-assign protected attributes: tag_names". When I put attr_accessible :tag_names instead, it seems to work okay, but this is a security issue, right? (Please note: there isn't a tag_names field in the DB for the Event objects.)
And why can't I replicate the Railscast? I'm running Rails 3.2.11, and the Railscast is from 2009, but I can't find anything saying that attr_writer has been replaced with attr_accessible in this later version or anything like that.
Thanks for any help!
The relevant part of my Event form:
<%= f.input :tag_names, label: "Tags (separate by commas)" %>
My Event model:
class Event < ActiveRecord::Base
include Taggable
# Default - order by start time
default_scope :order => 'events.start_time ASC'
belongs_to :creator, :foreign_key => "creator_id", :class_name => "User"
validates_presence_of :creator
(etc)
My Taggable module:
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, :as => :taggable
has_many :tags, :through => :taggings
attr_accessible :tag_names
end
def tag(name)
name.strip!
tag = Tag.find_or_create_by_name(name)
self.taggings.find_or_create_by_tag_id(tag.id)
end
def untag(name)
name.strip!
t = Tag.find_by_name(name)
self.taggings.find_by_tag_id(t).destroy
end
# Return an array of tags applied to this taggable object
def tag_list
Tag.joins(:taggings).where(taggings: {taggable_id: self})
end
# Getter method for virtual attribute tag_names
def tag_names
#tag_names || tags.map(&:name).join(', ')
end
# Setter method for virtual attribute tag_names
def tag_names=(names)
#tag_names = names.split(",").map do |n|
Tag.find_or_create_by_name(n.strip)
end
end
end
attr_accessible and attr_writer are two completely different things. The former is a concept pre-Rails 4 where you are whitelisting attributes that are mass-assignable. The latter is creating an instance method on your class that lets you set a value publicly, but not read it.
There are also attr_reader and attr_accessor.
attr_accessor is maybe what you're confusing with attr_accessible. This method is similar to attr_writer, except it provides both a reader and writer method. attr_reader is the opposite of attr_writer in that it gives you an instance method for reading values, but not writing them.

Mongoid and mass assignment

I have the following mogoid document definition/class:
class Exercise
include Mongoid::Document
field :name, :type => String
field :description, :type => String
belongs_to :group
validates_presence_of :name, :description
end
I then have the following controller and save method:
class ExercisesController < ApplicationController
respond_to :json
def create
#exercise = Exercise.create(params[:exercise])
#exercise.save!
respond_with #exercise
end
end
This seems wrong to me and open to mass assignment problems.
How do people normally protect against this and would using the strong parameters gem be a good idea?
Yes you should use the strong_parameters gem, it will be the default mass-assignment protection in rails 4
You can use attr_accessible as 'standard' protection. This of course still has the disadvantage that you expose a lot of fields to the interface, whereas you might want to expose only a few, but need to expose those fields in other controllers.

Rails ActiveRecord: validate single attribute

Is there any way I can validate a single attribute in ActiveRecord?
Something like:
ac_object.valid?(attribute_name)
You can implement your own method in your model. Something like this
def valid_attribute?(attribute_name)
self.valid?
self.errors[attribute_name].blank?
end
Or add it to ActiveRecord::Base
Sometimes there are validations that are quite expensive (e.g. validations that need to perform database queries). In that case you need to avoid using valid? because it simply does a lot more than you need.
There is an alternative solution. You can use the validators_on method of ActiveModel::Validations.
validators_on(*attributes) public
List all validators that are being used to validate a specific
attribute.
according to which you can manually validate for the attributes you want
e.g. we only want to validate the title of Post:
class Post < ActiveRecord::Base
validates :body, caps_off: true
validates :body, no_swearing: true
validates :body, spell_check_ok: true
validates presence_of: :title
validates length_of: :title, minimum: 30
end
Where no_swearing and spell_check_ok are complex methods that are extremely expensive.
We can do the following:
def validate_title(a_title)
Post.validators_on(:title).each do |validator|
validator.validate_each(self, :title, a_title)
end
end
which will validate only the title attribute without invoking any other validations.
p = Post.new
p.validate_title("")
p.errors.messages
#=> {:title => ["title can not be empty"]
note
I am not completely confident that we are supposed to use validators_on safely so I would consider handling an exception in a sane way in validates_title.
I wound up building on #xlembouras's answer and added this method to my ApplicationRecord:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def valid_attributes?(*attributes)
attributes.each do |attribute|
self.class.validators_on(attribute).each do |validator|
validator.validate_each(self, attribute, send(attribute))
end
end
errors.none?
end
end
Then I can do stuff like this in a controller:
if #post.valid_attributes?(:title, :date)
render :post_preview
else
render :new
end
Building on #coreyward's answer, I also added a validate_attributes! method:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def valid_attributes?(*attributes)
attributes.each do |attribute|
self.class.validators_on(attribute).each do |validator|
validator.validate_each(self, attribute, send(attribute))
end
end
errors.none?
end
def validate_attributes!(*attributes)
valid_attributes?(*attributes) || raise(ActiveModel::ValidationError.new(self))
end
end

Resources