Change property name of as_json call - ruby-on-rails

I have a model with the following method:
class Book < ActiveRecord:Base
def as_json(options = nil)
if options.nil?
super(only: [:id, :title, :status, :description])
else
super(options)
end
end
end
I also have a controller that looks like:
class BookController < ApplicationController
def search
books = Book.where(title: params[:keyword])
render json: books.as_json(only: [:id, :title])
end
end
Is it possible to override the :title symbol with another name that is needed by a 3rd party application? I'd like to change the :title to :value just for pushing out via this JSON call.
I've tried doing several different things to override (without writing special rules in as_json, as this is called from serveral different locations in the application).
Thanks in advance!

So, this the solution I ended up using:
JSON before .tap call:
[{"id"=>4, "book_code"=>"11-292454", "title"=>"How the world turns."}]
For single JSON object:
json = json.tap { |hash| hash["value"] = hash.delete "title" }
For JSON array:
json = json.each do |j|
j.tap { |hash| hash["value"] = hash.delete "title" }
end
JSON after .tap call:
[{"id"=>4, "book_code"=>"11-292454", "value"=>"How the world turns."}]
Don't know if it's the right way, but it seems to be working for me.

Related

Set a default value if no value is found in the API

I've created a little app based on an assignment that shows a random set of countries and facts about them, including the countries they share borders with. If they don't share any borders the field is currently empty which I would like to change to "I'm an island", or something.
https://countries-display.herokuapp.com/
This should be easy, but I'm very new, and am not sure how to approach it. Any help or points in the right direction would be greatly appreciated.
I tried initialising a default value:
class Country
include HTTParty
default_options.update(verify: false) # Turn off SSL verification
base_uri 'https://restcountries.eu/rest/v2/'
format :json
def initialize(countries = 'water')
#countries = countries
end
def self.all
#countries = get('/all')
#countries.each do |country|
country['borders'].map! do |country_code|
#countries.find { |country| country['alpha3Code'] == country_code } ['name']
end
country['languages'].map! { |language| language['name'] }
country['currencies'].map! { |currency| currency['name'] }
end
#countries
end
end
Setting a default in active record:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
class Country < ActiveRecord::Base
before_create :set_defaults
def set_defaults
self.countries_to_display = 'water' if #countries_to_display.nil?
end
end
I also tried implementing some if statements that were equally unsuccessful.
Solution:
#countries.each do |country|
if country['borders'].empty?
country['borders'] << "I'm an island"
else country['borders'].map! do |country_code|
#countries.find { |country| country['alpha3Code'] == country_code } ['name']
end
end
...
I'm assuming that country['borders'] maps to the field that should contain I'm an island. I'm also assuming that you directly render the output of Country.all.
What you'll need to do is to add a check whether there are any borders and if there aren't write that string to the field instead of the list of bordering countries.
#countries.each do |country|
if country['borders'].empty?
borders = "I'm an island"
else
borders = country['borders'].map do |country_code|
#countries.find { |country| country['alpha3Code'] == country_code }['name']
end
end
country['borders'] = borders
...
end
Note that we use map instead of map! here to not modify the existing collection. You may need to adjust your rendering logic, as country['borders'] previously contained a list of countries and now contains a string.

Forbidden Attributes Error when assigning nested attributes in Rails 4 using Postman

An AssessmentItem has many ItemLevels and one ItemLevel belongs to an AssessmentItem.
In my model I have
has_many :item_levels
accepts_nested_attributes_for :item_levels
When updating an Item, you should be able to specify what levels should be associated with that Item. The update action should receive the parameters specified for levels and create new ItemLevel objects that are associated with the Item being updated, and delete any levels that we previously associated and not specified when updating. However, when I try to create new levels, I get an ActiveModel::ForbiddenAttributes error.
Controller:
def update
#item = AssessmentItem.find(params[:id])
old_levels = #item.item_levels #active record collection
#item.update_attributes(update_params)
convert_levels = old_levels.map {|l| l.attributes} #puts into array
(items - convert_levels).each{|l| ItemLevel.create(l)} ##causes error
(convert_levels - level_params).each { |l| ItemLevel.find(l["id"]).destroy }
end
end
private
def level_params
params.require(:assessment_item).permit(:item_levels => [:descriptor, :level])
end
def update_params
params.require(:assessment_item).permit(:slug, :description, :name)
end
This is my json request in Postman:
{
"assessment_item": {
"slug" : "newSlug",
"description" : "NewDescriptiong",
"name" : "different name",
"item_level_attributes":[
{
"descriptor":"this should be new",
"level":"excellent"
}
]}
}
How can I get my action to allow the parameters? How can I effectively pass them to the factory? Thanks.
I think you should also permit item_level_attributes in update_params like this:
def update_params
params.require(:assessment_item).permit(:slug, :description, :name, :item_levels => [:descriptor, :level])
end
or
def update_params
params.require(:assessment_item).permit(:slug, :description, :name, :item_level_attributes => [:descriptor, :level])
end

How to return a json "object" instead of a json "array" with Active Model ArraySerializer?

Using Active Model Serializer, is there an easy and integrated way to return a JSON "object" (that would then be converted in a javascript object by the client framework) instead of a JSON "array" when serializing a collection? (I am quoting object and array, since the returned JSON is by essence a string).
Let's say I have the following ArticleSerializer:
class ArticleSerializer < ActiveModel::Serializer
attributes :id, :body, :posted_at, :status, :teaser, :title
end
I call it from ArticlesController:
class ArticlesController < ApplicationController
def index
#feed = Feed.new(articles: Article.all)
render json: #feed.articles, each_serializer: ArticleSerializer
end
end
Is there a way to pass an option to the serializer to make it return something like:
{"articles":
{
"1":{
...
},
"2":{
...
}
}
}
instead of
{"articles":
[
{
"id":"1",
...
},
{
"id":"2"
...
}
]
}
Edit: I guess that the approach proposed in this post (subclassing AMS ArraySerializer) might be helpful (Active Model Serializer and Custom JSON Structure)
You'd have to write a custom adapter to suit your format.
Alternatively, you could modify the hash before passing it to render.
If you do not mind iterating over the resulting hash, you could do:
ams_hash = ActiveModel::SerializableResource.new(#articles)
.serializable_hash
result_hash = ams_hash['articles'].map { |article| { article['id'] => article.except(:id) } }
.reduce({}, :merge)
Or, if you'd like this to be the default behavior, I'd suggest switching to the Attributes adapter (which is exactly the same as the Json adapter, except there is no document root), and override the serializable_hash method as follows:
def format_resource(res)
{ res['id'] => res.except(:id) }
end
def serializable_hash(*args)
hash = super(*args)
if hash.is_a?(Array)
hash.map(&:format_resource).reduce({}, :merge)
else
format_resource(hash)
end
end
No, semantically you are returning an array of Articles. Hashes are simply objects in Javascript, so you essentially want an object with a 1..n method that returns each Article, but that would not make much sense.

How to use attr_encrypted with as_json and join and get the decrypted attribute?

I have an attribute encypted using attr_encrypted and I'm using as_json. Under some circumstances I don't want the ssn to be part of a API response, and other times I want it to be included but using the name ssn not encrypted_ssn and to show the decrypted value. In all my cases encrypted_ssn should not be included in the result of as_json.
My first question is, how do I get as_json to return the decrypted ssn field?
With this code
class Person
attr_encrypted :ssn, key: 'key whatever'
end
I want this
Person.first.as_json
=> {"id"=>1,
"ssn"=>"333-22-4444"}
What I don't want is this:
Person.include_ssn.first.as_json
=> {"id"=>1,
"encrypted_ssn"=>"mS+mwRIsMI5Y6AzAcNoOwQ==\n"}
My second question is, how do I make it so a controller using a model can choose to include the decrypted ssn in the JSON ("ssn"=>"333-22-4444") or exclude the field (no "encrypted_ssn"=>"mS+mwRIsMI5Y6AzAcNoOwQ==\n")? I don't even want encrypted values going out to the client if the controller doesn't explicitly specify to include it.
This is what I have so far and seems to work:
class Person
attr_encrypted :ssn, key: 'key whatever'
scope :without_ssn, -> { select( column_names - [ 'encrypted_ssn' ]) }
default_scope { without_ssn }
end
Person.first.as_json
=> {"id"=>1}
I haven't figured out how to make this work in a way that includes the decrypted ssn field as in the first question. What I would like is something like this:
Person.include_ssn.first.as_json
=> {"id"=>1,
"ssn"=>"333-22-4444"}
My final question is, how do I make the above work through a join and how do I specify to include or exclude the encrypted value (or scope) in the join?
With this code:
class Person
has_many :companies
attr_encrypted :ssn, key: 'key whatever'
scope :without_ssn, -> { select( column_names - [ 'encrypted_ssn' ]) }
default_scope { without_ssn }
end
class Company
belongs_to :person
end
This seems to work like I want it
Company.where(... stuff ...).joins(:person).as_json(include: [ :person ])
=> {"id"=>1,
"person"=>
{"id"=>1}}
But I don't know how to implement include_ssn like below or alternatives to tell the person model to include the ssn decrypted.
Company.where(... stuff ...).joins(:person).include_ssn.as_json(include: [ :person ])
=> {"id"=>1,
"person"=>
{"id"=>1,
"ssn"=>"333-22-4444"}}
I've solved this in a different way. Originally I was doing this:
app/models/company.rb
class Company
# ...
def self.special_get_people
people = Company.where( ... ).joins(:person)
# I was doing this in the Company model
people.instance_eval do
def as_json_with_ssn
self.map do |d|
d.as_json(except: [:encrypted_ssn] ).merge('ssn' => d.person.ssn)
end
end
def as_json(*params)
if params.empty?
super(except: [:encrypted_ssn] ).map{ |p| p.merge('ssn' => nil) }
else
super(*params)
end
end
end
return people
end
end
app/controllers/person_controller.rb
class PersonController < ApplicationController
def index
#people = Company.special_get_people
# Then manually responding with JSON
respond_to do |format|
format.html { render nothing: true, status: :not_implemented }
format.json do
render json: #people.as_json and return unless can_view_ssn
render json: #people.as_json_with_ssn
end
end
end
end
However this was fragile and error prone. I've since refactored the above code to look more like this:
app/models/company.rb
class Company
# ...
def self.special_get_people
Company.where( ... ).joins(:person)
end
end
app/controllers/person_controller.rb
class PersonController < ApplicationController
def index
#people = Company.special_get_people
end
end
app/views/person/index.jbuilder
json.people do
json.array!(#people) do |person|
json.extract! person, :id # ...
json.ssn person.ssn if can_view_ssn
end
end
And this ends up being a much better solution that's more flexible, more robust and easier to understand.

How to pass complex object to swagger param function?

When I create auto documented API specification, I faced with problem of passing complex object (ActiveRecord for ex.) to param function of swagger-docs/swagger-ui_rails, because it takes only simple types (string, integer, ...).
I solved this trouble with next metaprogramming ruby trick:
class Swagger::Docs::SwaggerDSL
def param_object(klass, params={})
klass_ancestors = eval(klass).ancestors.map(&:to_s)
if klass_ancestors.include?('ActiveRecord::Base')
param_active_record(klass, params)
end
end
def param_active_record(klass, params={})
remove_attributes = [:id, :created_at, :updated_at]
remove_attributes += params[:remove] if params[:remove]
test = eval(klass).new
test.valid?
eval(klass).columns.each do |column|
unless remove_attributes.include?(column.name.to_sym)
param column.name.to_sym,
column.name.to_sym,
column.type.to_sym,
(test.errors.messages[column.name.to_sym] ? :required : :optional),
column.name.split('_').map(&:capitalize).join(' ')
end
end
end
end
Now I can use param_object for complex objects as param for simple types :
swagger_api :create do
param :id, :id, :integer, :required, "Id"
param_object('Category')
end
Git fork here:
https://github.com/abratashov/swagger-docs

Resources