TTOVARISCHH
TTOVARISCHH

Reputation: 59

Create a nested object through a POST request (RoR)

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)

Upvotes: 1

Views: 210

Answers (2)

max
max

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

jamesc
jamesc

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

Related Questions