1

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

UPDATE: error from postman

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: from postman again

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)
  • where is your permit method to set your allowable params? You are not showing your full controller code so it's difficult to help really, please post the rest of your controller code particularly the private section that sets up the missing params method that your error is complaining about. It's probabaly just a typo should look something like this `params.require(:` – jamesc Feb 24 '23 at 23:18
  • Hello, jamesc! Welp, actually, just a minute ago I figured out what was the problem. I am not sure that this was the right decision, but another stackoverflow answer advised to get rid of [:player] in controller at all. Updated the question with a new code! However the problem is that all the new players that I create with a written "name" (for example) don't get name. Players exist, but all the values inside are NULL – TTOVARISCHH Feb 24 '23 at 23:25
  • you've commented out your player params so when you send params keyed by player there won't be any picked up by the controller, I can see you are permitting them in the create method but that is not the correct place – jamesc Feb 24 '23 at 23:30
  • Checkout the accepted answer on this question that describes how to permit nested params https://stackoverflow.com/questions/18436741/rails-strong-parameters-nested-objects – jamesc Feb 24 '23 at 23:35
  • Also, you should be including the game params in the request you send to the controller – jamesc Feb 24 '23 at 23:36
  • in `@game = Game.find_by_code(params[:game_id])` you are not supplying a game_id in your params – jamesc Feb 24 '23 at 23:42
  • I uncommented player_params and brought back .require(:player) but it throws the same stupid error "param is missing or the value is empty: player". Now sure how it's possible... – TTOVARISCHH Feb 24 '23 at 23:42
  • Post the exact error and full stack trace please – jamesc Feb 24 '23 at 23:43
  • But GET request with the same path works just fine :( The game and it's players are found and displayed – TTOVARISCHH Feb 24 '23 at 23:44
  • But you are not passing a game ID in the params so hopw can the get_game method work without that and if you don't have agame then you have a nil class and you can't permit params. Need to see the full stack trace please – jamesc Feb 24 '23 at 23:45
  • You show your request params as being player params, these should be nested inside a game in order for the game to be found by game id – jamesc Feb 24 '23 at 23:47
  • Updated the question! Sorry, if I don't understand you properly :( I am not sure how to pass game ID to request params(( – TTOVARISCHH Feb 24 '23 at 23:53
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/252117/discussion-between-jamesc-and-ttovarischh). – jamesc Feb 24 '23 at 23:59
  • @jamesc you haven't grasped the difference between a nested route and nested attributes. I understand that you're trying to help here but you're really just adding confusion here. – max Feb 25 '23 at 10:38

2 Answers2

1

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
max
  • 96,212
  • 14
  • 104
  • 165
  • OH MY GOD! You are my saviour... Thank you so much for a detailed answer! And thank you very much for adviсe. It works just perfect right now! Thanks!!! – TTOVARISCHH Feb 25 '23 at 16:09
-1

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

jamesc
  • 12,423
  • 15
  • 74
  • 113
  • This should not be wrapped inside `"game"`. The `game_id` (code) is passed as a routing param e.g. `.../games/:game_id/...` so you don't need to pass it as part of the body. The OP is calling `require(:player)` so the player key will need to be a top level node or the code will fail the same way. – engineersmnky Feb 25 '23 at 04:22