I’ve been trying to find and an easy way to serialize all of the errors to JSON in Rails 6. Not writing the custom ones, but just add a standard JSON format for them. Wrote a serializer like:
class ErrorSerializer
def initialize(error)
#error = error
end
def to_h
serializable_hash
end
def to_json(_payload=nil)
to_h.to_json
end
private
def serializable_hash
{
errors: [error.serializable_hash].flatten
}
end
attr_reader :error
end
And in my User controller, tried to render error with:
render json: ErrorSerializer.new(#user.errors.to_hash), status: :bad_request
But getting an error:
NoMethodError (undefined method `serializable_hash')
Has anyone encountered something similar? And, in general, what is the best/easiest way to serialize all errors to JSON in Rails 6 (ideally, if there won’t be a need to raise/render errors on each endpoint in the controller)
In case someone needs some simple errors serializer. This worked fine for me:
class ActiveRecordErrorsSerializer
attr_reader :object
def initialize(object)
#object = object
end
def to_h
serializable_hash
end
def as_json(_payload = nil)
{ data: { type: 'errors' }, attributes: to_h }
end
private
def serializable_hash
object.errors.messages.flat_map do |field, errors|
errors.flat_map do |error_message|
{
source: { pointer: "/data/attributes/#{field}" },
details: error_message
}
end
end
end
end
Related
I am trying to write the allow method in RSpec. My rails controller is
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
if response['code'] == 200
if User.first.update(user_params)
render json: { message: "User successfully updated"}, status: :ok
else
head :unprocessable_entity
end
else
render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
end
end
private
def send_request_to_update_in_comapny(payload)
response = Api::V1::CompanyRequestService.new(
payload: payload.merge(company_api_access_details),
url: 'customers/update_name_email',
request_method: Net::HTTP::Post
).call
JSON.parse(response.body)
end
end
end
When I write the bellow code in my test file
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company).and_return({ 'code' => 500 })
I am getting the following error in terminal
Users::ProfilesController does not implement: send_request_to_update_in_comapny
enter code here
With allow_any_instance_of I am able to get the code working. But how can I implement it using allow?
Yes, allow_any_instance_of works because, as the name suggests, it allows any instance of Users::ProfilesController to respond to the instance method send_request_to_update_in_company with your mock return value.
However, your line
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company)
is telling RSpec to mock a class method called send_request_to_update_in_company, which doesn't exist. And so, you're seeing the error message saying so.
You don't say where your test is situated, but generally wherever it is, it's not a good idea to either test or stub out a private method.
I'd be inclined to instead create a mock Api::V1::CompanyRequestService object to return a fake response, which your controller code can then parse as expected and produce the expected JSON. For example
mock_request = instance_double(Api::V1::CompanyRequestService)
allow(mock_request).to receive(:call).and_return('{"code": 500}')
allow(Api::V1::CompanyRequestService).to receive(:new).and_return(mock_request)
Another approach might be to leave your service alone, and instead use tools like VCR or WebMock to provide mocked JSON values at the network layer - your code can think it's calling out to the internet, but really it gets back responses that you define in your tests.
How about this way:
spec/requests/users/profiles_controller_spec.rb
require 'rails_helper'
RSpec.describe "Users::ProfilesControllers", type: :request do
describe "Test call to special function: " do
let(:controller) { Users::ProfilesController.new }
it "Should response to code 500" do
response = controller.send_request_to_update_in_company("test")
expect(response).to eq({"code"=>"500", "test1"=>"abc", "test2"=>"def"})
end
it "Should return to true" do
response = controller.true_flag?
expect(response).to eq(true)
end
end
end
app/controllers/users/profiles_controller.rb
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
Rails.logger.debug "Ok71 = response['code'] = #{response['code']}"
# if response['code'] == 200
# if User.first.update(user_params)
# render json: { message: "User successfully updated"}, status: :ok
# else
# head :unprocessable_entity
# end
# else
# render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
# end
end
# Not private, and not mistake to 'send_request_to_update_in_comapny'
def send_request_to_update_in_company(payload)
response = Api::V1::CompanyRequestService.new(
payload: "for_simple_payload_merge_values",
url: 'for_simple_customers/update_name_email',
request_method: "for_simple_request_method"
).call
Rails.logger.debug "Ok66 = Start to log response"
Rails.logger.debug response
JSON.parse(response.body)
end
# Simple function to test
def true_flag?
true
end
end
end
app/services/api/v1/company_request_service.rb
class Api::V1::CompanyRequestService < ActionController::API
def initialize(payload="test1", url="test2", request_method="test3")
#payload = payload
#url = url
#request_method = request_method
end
def call
#object = Example.new
#object.body = {code: "500", test1: "abc", test2: "def"}.to_json
return #object
end
end
class Example
attr_accessor :body
def initialize(body={code: "000", test1: "init_value_abc", test2: "init_value_def"}.to_json)
#body = body
end
end
I use simple code to simulate your project. Modify it to suitable your working! Tell me about your its thinking. Thank you!
I have a controller that accepts three params, title, users and project_type. I want to make all the params required
I have seen people do things like
def project_params
params.require(:title,:project_type, :users)
.permit(:title, :project_type, :users)
end
And then do Project.new(project_params), but I need to work a little with the params first. How can I make this possible?
I make a post request in postman like this:
module Api
module V1
class ProjectsController < ApplicationController
def create
admins = params[:admins]
users = get_user_array()
project_type = ProjectCategory.find_by(name: params[:project_type])
project = Project.new(
title: params[:title],
project_category: project_type,
project_users: users)
if project.save
render json: {data:project}, status: :ok
else
render json: {data:project.errors}, status: :unprocessable_entity
end
end
...
end
end
end
{
"title": "Tennis",
"project_type": "Sports",
"users": [{"name": "john Dow", "email": "johnDoe#gmail.com"}],
}
I would say that you are using ActionController::Parameters#require wrong. Its not meant to validate that the all the required attributes are present - thats what model validations are for. Rather you should just use params.require to ensure that the general structure of the parameters is processable.
For example if you used the rails scaffold you would get the following whitelist:
params.require(:project)
.permit(:title, :project_type)
This is because there is no point in continuing execution if the project key is missing from the params hash since this would give you an empty hash or nil.
ActionController::Parameters#require will raise a ActionController::ParameterMissing error which will return a 400 - Bad Request response which is the wrong response code for what you are doing. You also should not use exceptions for normal application flow. A missing attribute is not an exceptional event.
Instead if you want to use a flat params hash you should whitelist it with:
def project_params
params.permit(:title, :project_type, users: [:name, :email])
end
I think that if you don't have to get anything from the frontend to run get_user_array(), you could only allow and require title and project_type.
def create
users = get_user_array()
project = Project.new(project_params)
project.users = users
if project.save
render json: {data:project}, status: :ok
else
render json: {data:project.errors}, status: :unprocessable_entity
end
end
private
def project_params
params.require(:project).permit(:title, :project_type).tap do |project_params|
project_params.require(:title, :project_type)
end
end
If you need to process something before creating the project, you can do this:
project_category = ProjectCategory.find_by(name: project.project_type)
I am using serializer to format json response of my rails-api projector.
I am using a concern to format final response.
My code snippets are as follows
entry_controller.rb
class EntriesController < ApplicationController
include Response
def index
#entries = #current_user.entries
json_response(#entries)
end
end
concerns/response.rb
module Response
def json_response(response, error = nil, message = 'Success', code = 200)
render json: {
code: code,
message: message,
error: error,
response: response
}
end
end
application_serializer.rb
class ApplicationSerializer < ActiveModel::Serializer
end
entry_serializer.rb
class EntrySerializer < ApplicationSerializer
attributes :title, :content, :is_encrypted, :entry_date
end
In entries#index if i use json_response(#entries) my final response of request is not formatted and each entry is as in database. instead if i use render json: #entries. Im getting as per serializer. I want to use concern method json_response(#entries) along with serializers. Can someone suggest a way to use serializers in concern methods in a generic way as multiple controllers use same concern method.
Thanks in advance.
Something related to serializer params is what you want to customise your response.
class EntriesController < ApplicationController
include Response
def index
#entries = #current_user.entries
render json: #entries, serializer_params: { error: nil, message: "Success", code: 200}
end
end
class EntrySerializer < ApplicationSerializer
attributes :title, :content, :is_encrypted, :entry_date
params :error, :message, :code
def attributes
super.merge(:error, :message, :code)
end
end
I’m not sure I fully understand your question, but I don’t believe render :json calls to_json recursively if it is given a hash, like in this case. So you may be looking for something like this in your concern:
module Response
def json_response(response, error = nil, message = 'Success', code = 200)
render json: {
code: code,
message: message,
error: error,
response: response.to_json
}
end
end
I have 2 non database attributes in my model. If one of them has a value, I need to return the other one in the json response:
class Car < ApplicationRecord
attr_accessor :max_speed_on_track
attr_accessor :track
def attributes
if !self.track.nil?
super.merge('max_speed_on_track' => self.max_speed_on_track)
end
end
end
The problem is that the line 'if !self.track.nil?' throws an error when the controller tries to return the json
Perhaps there is a better way as I read that using attr_accessor is a code smell.
What I am trying to do is if the user passes me a track value as a query parameter, then I pass that value to the model and it uses it to calculate the max_speed_on_track, and return that value.
Obviously if no track is provided by the user then I don't want to return max_speed_on_track in the json.
The controller method is very basic for now (I still need to add the code that checks for the track param). The code throws the error on the save line.
def create
#car = Car.new(car_params)
if #car.save
render json: #car, status: :created
else
render json: #car.errors, status: :unprocessable_entity
end
end
Try this out:
class Car < ApplicationRecord
attr_accessor :max_speed_on_track
attr_accessor :track
def as_json(options = {})
if track.present?
options.merge!(include: [:max_speed_on_track])
end
super(options)
end
end
Since Rails uses the attributes method, and you're only needing this for json output, you can override the as_json method just like in this article. This will allow you to include your max_speed_on_track method in your json output when the track is present (not nil).
Can anyone help with this?Thanks in advance for your reply.
In app/controllers/heats_controller.rb, the code is as below:
class HeatsController < ApplicationController
def add_car
Pusher[params[:sendTo]].trigger('addCar', car_params)
head :ok
end
def index
#heats = Heat.all
render json: #heats
end
def new
race = Race.all.sample
render json: { start_time: Time.now.to_s,
race_id: race.id,
text: race.passage }
end
def show
#heat = Heat.find(params[:id])
end
def start_game
Pusher[params[:sendTo]].trigger('initiateCountDown', start_heat_params)
head :ok
end
def update_board
Pusher[params[:channel]].trigger('updateBoard', car_params)
head :ok
end
private
def heat_params
params.require(:heat).permit(:race_id)
end
def car_params
params.permit(:racer_id,
:racer_name,
:return_to,
:progress,
:racer_img,
:wpm,
:channel,
:sendTo)
end
def start_heat_params
params.permit(:channel, :race_id, :text, :timer)
end
end
In app/models/heat.rb, the code is as below:
class Heat < ActiveRecord::Base
belongs_to :race
has_many :racer_stats
end
Any help will be appreciated ,thank you
Error:
Processing by HeatsController#new as */*
Completed 500 Internal Server Error in 6ms
NoMethodError (undefined method `id' for nil:NilClass):
app/controllers/heats_controller.rb:18:in `new'
It seems that Race.all.sample does not work.
Maybe you have no records in races table at all.
Also, try this to retrieve random record (for Rails 4):
def new
offset = rand(Race.count)
Race.offset(offset).first
...
end
The error is here:
def new
... race_id: race.id ...
end
race doesn't have any data, so calling .id on it won't work. You're also calling Race.all.sample (very bad), which means you've probably got no records in your Race model.
If you want to pick a random record, you should use this answer to pull one:
def new
race = Race.order("RAND()").first #-> MYSQL
render json: { start_time: Time.now.to_s, race_id: race.id, text: race.passage } unless race.nil?
end
You could do:
render json: { start_time: Time.now.to_s,
race_id: race.try(:id),
text: race.try(:passage) }
This would fix the whiny nil error if your .sample returns nil.
But I agree that all.sample is bad practice.