Reputation: 59
I am very very new to Ruby on Rails, so sorry, if something is obviosly wrong or stupid :( But as a part of my university project I am supposed to create an app that uses RoR API. Everything was quite fine till this day. I have a model Game that has many nested Players inside. I am trying to build a POST request to create a new Player inside some Game. As "rails routes" says my path should be like "/api/v1/games/:game_id/players", in Postman I'm sending the following request to "http://localhost:3000/api/v1/games/HB236/players".
{
"player": {
"name": "GOBLIN BOBLIN",
}
}
However I get an error — NoMethodError in PlayersController#create undefined method `permit' for nil:NilClass. I am trying to understand — what am I doing wrong? Here are some of my files:
players_controller.rb
class PlayersController < ApplicationController
respond_to :json
before_action :get_game
def create
# @game = Game.find_by_code(params[:game_id])
@player = @game.players.create(params.permit(:name, :hp, :initiative, :languages, :perc, :inv, :ins, :armor, :conc))
render json: @player
# render json: @game.players
end
def index
# @game = Game.find_by_code(params[:game_id])
@players = @game.players
render json: @players
end
def get_game
@game = Game.find_by_code(params[:game_id])
end
# private
# def player_params
# params.require(:player).permit(:name, :hp, :initiative, :languages, :perc, :inv, :ins, :armor, :conc)
# end
end
player.rb
class Player < ApplicationRecord
belongs_to :game
end
poorly written routes.rb
Rails.application.routes.draw do
get 'welcome/index'
get 'effects/index'
get 'games/index'
post "", to: "welcome#redirect", as: :redirect
root 'welcome#index'
resources :games do
resources :players
resources :monsters
end
scope '/api/v1' do
resources :games, only: [:index, :show, :create, :update, :destroy] do
resources :players, only: [:index, :show, :create, :update, :destroy]
end
resources :effects, only: [:index, :show, :create, :update, :destroy]
resources :slug
resources :users, only: %i[index show]
end
scope :api, defaults: { format: :json } do
scope :v1 do
devise_for :users, defaults: { format: :json }, path: '', path_names: {
sign_in: 'login',
sign_out: 'logout',
registration: 'signup'
},
controllers: {
sessions: 'api/v1/users/sessions',
registrations: 'api/v1/users/registrations'
}
end
end
end
And also messages from the server:
app/controllers/players_controller.rb:7:in `create'
Started POST "/api/v1/games/HB236/players" for ::1 at 2023-02-25 02:40:28 +0300
Processing by PlayersController#create as */*
Parameters: {"game_id"=>"HB236"}
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/players_controller.rb:17:in `get_game'
Game Load (0.2ms) SELECT "games".* FROM "games" WHERE "games"."code" = ? LIMIT ? [["code", "HB236"], ["LIMIT", 1]]
↳ app/controllers/players_controller.rb:17:in `get_game'
Completed 400 Bad Request in 52ms (ActiveRecord: 1.6ms | Allocations: 8862)
ActionController::ParameterMissing (param is missing or the value is empty: player):
app/controllers/players_controller.rb:22:in `player_params'
app/controllers/players_controller.rb:7:in `create'
And also this is a response from GET request:
Started GET "/api/v1/games/HB236/players" for ::1 at 2023-02-25 02:51:52 +0300
Processing by PlayersController#index as */*
Parameters: {"game_id"=>"HB236"}
(0.2ms) SELECT sqlite_version(*)
↳ app/controllers/players_controller.rb:17:in `get_game'
Game Load (0.5ms) SELECT "games".* FROM "games" WHERE "games"."code" = ? LIMIT ? [["code", "HB236"], ["LIMIT", 1]]
↳ app/controllers/players_controller.rb:17:in `get_game'
Player Load (1.4ms) SELECT "players".* FROM "players" WHERE "players"."game_id" = ? [["game_id", 1]]
↳ app/controllers/players_controller.rb:14:in `index'
Completed 200 OK in 73ms (Views: 44.4ms | ActiveRecord: 4.0ms | Allocations: 11492)
Upvotes: 1
Views: 210
Reputation: 102250
Lets start by fixing the controller:
class PlayersController < ApplicationController
before_action :set_game # this method actually sets an instance variable
# POST /api/v1/games/AB123/players
def create
# Don't just assume that the user will pass valid input
@player = @game.players.new(player_params)
if @player.save
render json: @player,
status: :created # pay attention to sending the correct status codes so the client knows what happened
else
render json: { errors: @player.errors.full_messages },
status: :unprocessable_entity
end
end
# GET /api/v1/games/AB123/players
def index
@players = @game.players
render json: @players
end
private
# This method should use the "bang" method
# which will raise a exception and cause a 404 not found response if
# the game code is not valid
def set_game
@game = Game.find_by_code!(params[:game_id])
end
def player_params
params.require(:player)
.permit(
:name, :hp, :initiative, :languages,
:perc, :inv, :ins, :armor, :conc
)
end
end
This uses the conventional Rails parameter whitelist which expects either this JSON in the body:
{
"player": {
"name": "Grok",
"hp": 10
}
}
But it can also take "flat" parameters if parameters wrapping is turned on (it is by default for JSON requests).
{
"name": "Grok",
"hp": 10
}
There is no difference between the parameters sent in the body for nested/non-nested routes as the parent id is sent through the URI.
However you can actually structure your parameters however you want in an API. The Rails conventions are mainly useful for classical applications or mixed mode apps as they work together with the form helpers. The don't actually make that much sense for a pure API. You may want to consider using the JSONAPI.org schema instead.
def player_params
params.require(:data)
.require(:attributes)
.permit(
:name, :hp, :initiative, :languages,
:perc, :inv, :ins, :armor, :conc
)
end
Instead of using Postman write actual integration tests with Minitest or RSpec. Postman is good tool for debugging external APIs but you're wasting your time when you're using it on your own application as you can spend that time safeguarding your application against future regressions.
require "test_helper"
class PlayersApiTest < ActionDispatch::IntegrationTest
setup do
@game = games(:one) # a record thats setup via fixtures
end
test "creating a game with valid parameters" do
assert_difference '@game.players.count', 1 do
post "/api/v1/games/#{@game.code}/players",
parameters: {
game: {
name: "Grok"
hp: 10
}
}
end
assert_response :created
end
# ...
end
Upvotes: 1
Reputation: 12837
You have a nested route but you are not formatting the params properly when you send your request to the server so the player params do not exist. You also have added some problems when changing code that did not need changing as discussed and corrected in our chat.
Parameters: {"game_id"=>"HB236"} clearly shows no player params are being received
So something like
"game": {
"code": "HB236",
"player": {
"name": "Testing",
"user_id": 4
}
}
Should lead you to something close
Upvotes: -1