I am making a rails json api which uses service objects in controllers actions and basing on what happend in service I have to render proper json. The example looks like this.
star_service.rb
class Place::StarService
def initialize(params, user)
#place_id = params[:place_id]
#user = user
end
def call
if UserStaredPlace.find_by(user: user, place_id: place_id)
return #star was already given
end
begin
ActiveRecord::Base.transaction do
Place.increment_counter(:stars, place_id)
UserStaredPlace.create(user: user, place_id: place_id)
end
rescue
return #didn't work
end
return #gave a star
end
private
attr_reader :place_id, :user
end
places_controller.rb
def star
foo_bar = Place::Star.new(params, current_user).call
if foo_bar == #sth
render json: {status: 200, message: "sth"}
elsif foo_bar == #sth
render json: {status: 200, message: "sth"}
else
render json: {status: 400, message: "sth"}
end
And my question is, if I should return plain text from service object or there is some better approach?
It'll be opinionated of course but still...
Rendering views with data, returning data, redirecting etc are the responsibilities of controllers. So any data, plain text and other things you have to handle in your controller.
Service object have to provide one single public method for any huge complex operation performing. And obviously that method has to return simple value which tells controller if operation was completed successfully or not. So it must be true or false. Maybe some recognizable result (object, simple value) or errors hash. It's the ideal use case of course but it's the point.
As for your use case your service may return the message or false. And then controller will render that message as json.
And your star method must live in your controller, be private probably and looks like that:
def star
foo_bar = Place::Star.new(params, current_user).call
if foo_bar
render json: {status: 200, message: foobar}
else
render json: {status: 400, message: "Failed"}
end
end
Your Service:
class Place::StarService
def initialize(params, user)
#place_id = params[:place_id]
#user = user
end
def call
if UserStaredPlace.find_by(user: user, place_id: place_id)
return "Message when star is already given"
end
begin
ActiveRecord::Base.transaction do
Place.increment_counter(:stars, place_id)
UserStaredPlace.create(user: user, place_id: place_id)
end
rescue
return false
end
return "Message if gave a star"
end
private
attr_reader :place_id, :user
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)
How do I check if a record already exists in my db when posting with Ajax?
Here is my Ajax code:
$.ajax({
type: "POST",
url: "team_selections#create",
data: {
team_selection: {
season_id: "1",
club_id: "1",
player_id: id,
fixture_week: "1",
position: pos
}
},
dataType: "html"
})
Here is my Rails controller code:
def create
if !TeamSelection.where(season_id: params[:season_id], club_id: params[:club_id], player_id: params[:player_id], fixture_week: params[:fixture_week], position: params[:position]).exists?
TeamSelection.create(selection_params)
end
end
private
def selection_params
params.require(:team_selection).permit(:season_id, :club_id, :player_id, :fixture_week, :position)
end
you can use find_or_create_by rails method in your controller. this will finds the first record with the given attributes, or creates a record with the attributes if one is not found.This method always returns a record, but if creation was attempted and failed due to validation errors it won’t be persisted, you get what create returns in such situation.
def create
TeamSelection.find_or_create_by(selection_params)
end
You can add a check with the help of a before_action.
before_action :check_record_exists?
def create
TeamSelection.create(selection_params)
render_something
end
private
def check_record_exists?
if TeamSelection.where(selection_params.slice(:season_id, :club_id, :player_id, :fixture_week, :position)).exists?
render json: { error: 'Record already exists' }
end
end
def selection_params
params.require(:team_selection).permit(:season_id, :club_id, :player_id, :fixture_week, :position)
end
NOTE: You definitely need to have a validation on model to prevent creation of such records. Don't just rely on checks in controller or the JS.
As #Jagdeep commented correctly: add a validation in the model if you don't want similar records to be created more than once.
But here controller is not returning any response like 'Record already exists'
Replace your create method with
def create
is_record_present = TeamSelection.where(season_id: params[:season_id], club_id: params[:club_id], player_id: params[:player_id], fixture_week: params[:fixture_week], position: params[:position]).exists?
if !is_record_present
TeamSelection.create(selection_params)
else
#return respose for example
render json: {message: 'Record already present'}, status: :bad_request
end
end
I have several actions in my hotel_controller where I call an API to get back data. I created different services to keep my API calls outside my controller logic. For every API calls I have got some "general response errors" like unauthorized or not found for instance. As these errors are common to all API calls, I wanted to create a private method to deal with them in my hotel controller:
private
def global_error_checking(response)
if response.message == "Unauthorized"
redirect_to unauthorized_path and return
elsif response.message == "Not Found"
redirect_to not_found_path and return
else
end
end
Then in every method of my controller where it's needed I would call the global_error_checking method before checking for specific errors. For instance :
def index
service = Hotels::GetHotelListService.new( account_id: params[:account_id],
user_email: session[:user_email],
user_token: session[:user_token]
)
#response = service.call
global_error_checking(#response)
if #response["hotels"].blank?
flash[:notice] = "You have not created any hotels yet !"
redirect_to account_path(params[:account_id])
else
#hotels = #response["hotels"]
#account = #response["account"]
end
end
The problem is that after executing global_error_checking, the action of the controller goes on and does not stop even though a condition of global_error_checking is satisfied.
1) How can I stop the execution of the whole controller method if a condition inside global_error_checking is satisfied ?
2) Is there maybe a better way to achieve this ?
I wouldn't name the parameter "response" since that's already being used by the controller.
The other thing I noticed is that you're accessing this "#response" in different ways which might be ok but it looks wrong. In your global_error_checking method you're accessing it's properties using dot syntax (response.message), however in your controller action you're accessing it as if it were a hash. Again, this might be ok depending on its data type.
If I were you, I would refactor this to look like:
class SomeController < ApplicationController
def index
#hotels = some_resource['hotels']
#account = some_resource['account']
end
private
def some_resource
#_some_resource ||= begin
service = Hotels::GetHotelListService.new({
account_id: params[:account_id],
user_email: session[:user_email],
user_token: session[:user_token]
})
result = service.call
if result['message'] == 'Unauthorized'
redirect_to unauthorized_path and return
elsif result['message'] == 'Not Found'
redirect_to unauthorized_path and return
else
result
end
end
end
end
You can use return statement:
return if global_error_checking in your controller
and case statement in private method with some changes:
private
#returns true after redirecting if error message "Unauthorized" or "Not Found"
def global_error_checking(response)
case response.message
when "Unauthorized"
redirect_to unauthorized_path and return true
when "Not Found"
redirect_to not_found_path and return true
end
end
I have a user model and an email model. The user can create emails and receive them. A received email is realy just an email, but joined to the user on different field. My setup is below, but it doesn't work correctly. When I call received_messages on a user that created an email (and did not receive it) he also sees a message.
Is this possible at all this way?
class User < ActiveRecord::Base
has_many :mail_messages
has_many :received_messages, class_name: 'MailMessage', foreign_key: 'to_id'
end
class MailMessage < ActiveRecord::Base
belongs_to :user
emd
I did create a controller for these messages:
class ReceivedEmailsController < ApplicationController
before_filter :authenticate_user!
def index
messages = current_user.received_messages
if messages
render json: messages, each_serializer: MailMessageSerializer, status: :ok
else
render json: :nothing, status: :not_found
end
end
def show
message = current_user.received_messages.where(id: params[:id]).first
if message
render json: message, serializer: MailMessageSerializer, status: :ok
else
render json: :nothing, status: :not_found
end
end
end
The tests:
describe ReceivedEmailsController do
let!(:main_user) { Fabricate :user }
let!(:receiving_user) { Fabricate :user }
let!(:message) { Fabricate :mail_message, user_id: main_user.id, to_id: receiving_user.id }
describe 'get index' do
it 'should give not found when there are no emails received' do
main_user.confirm!
sign_in main_user
get :index
JSON.parse(response.body)['received_emails'].size.should eq 0
end
end
end
Instead of 0 I get 1 back, the same message that I get back when doing the test via the receiving_user. I want to get the message back via the receiving_user but not via the main_user.
I'm not sure whether it can be done this way, whether to use scopes, or even something else I'm not aware of.
Thanks.
One of your let! statements fabricates an email which belongs to main_user, so you get, correctly one result.
UPDATE
The above is incorrect. Try if messages.any? instead of if messages as it will always return true, since associations returns an array (empty array if there are no matching records)