hanami / controller

Complete, fast and testable actions for Rack and Hanami

Home Page:http://hanamirb.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Nested params are stringified during params validation

nickgnd opened this issue · comments

Hi everybody, thanks again for your works!

I'm trying Hanami for a json API backend and I noticed a weird behavior in an action. It seems that the validation stringified the nested params' keys, so I can not access to them via symbol as explained in the guide, but I've to use a "mixed" approach: params.get(:items, 'code').

Without params validation all works fine.

A repository with a basic app to show the behavior could be found here

Below some code extracts from the app

my env:

  • ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin15]
  • hanami v1.0.0.beta1

Gemfile

source 'https://rubygems.org'

gem 'rake'
gem 'hanami',       '1.0.0.beta1'
gem 'hanami-model', '~> 1.0.0.beta1'

gem 'sqlite3'

group :development do
  # Code reloading
  # See: http://hanamirb.org/guides/projects/code-reloading
  gem 'shotgun'
end

group :test, :development do
  gem 'dotenv', '~> 2.0'
  gem 'byebug'
end

group :test do
  gem 'minitest'
  # gem 'capybara'
  gem 'rack-test'
end

group :production do
  # gem 'puma'
end

web/application.rb

module Web
  class Application < Hanami::Application
    configure do
      # ...
      default_request_format :json
      default_response_format :json
      body_parsers :json
      # ...
      end
    end
  end
end

web/config/routes.rb

post '/items', to: 'items#create'

web/controllers/items/create.rb

module Web::Controllers::Items
  class Create
    include Web::Action

    params do
      required(:item).schema do
        required(:code).filled(:str?)
        required(:available).filled(:bool?)
      end
    end

    def call(params)
      halt 401 unless params.valid?

      ###
      # byebug
      puts '#' * 10
      puts params.inspect
      puts "\n"
      puts params.get(:item)
      puts '#' * 10
      #
      ###

      # this works fine for the controller test
      #
      code = params.get(:item, :code)
      available = params.get(:item, :available)


      # this works fine for the feature test and http requests
      #
      # code = params.get(:item, 'code')
      # available = params.get(:item, 'available')

      item = ItemRepository.new.create(code: code, available: available)
      status 201, ''
    end
  end
end

When I launch the controller test, it pass and the params are accessible via symbol:

spec/web/controllers/items/create_spec.rb

require 'spec_helper'
require_relative '../../../../apps/web/controllers/items/create'

describe Web::Controllers::Items::Create do
  let(:action) { Web::Controllers::Items::Create.new }
  let(:params) { { item: { code: 'ABCD', available: true }} }

  it 'is successful' do
    response = action.call(params)
    response[0].must_equal 201
  end
end

and the output from the puts in the controller is:

rake test TEST=spec/web/controllers/items/create_spec.rb                                                                                                                    
Run options: --seed 41506

# Running:

##########
#<Hanami::Action::BaseParams:0x007fcfcffdff38 @env={:item=>{:code=>"ABCD", :available=>true}}, @raw={:item=>{:code=>"ABCD", :available=>true}}, @params={:item=>{:code=>"ABCD", :available=>true}}>

{:code=>"ABCD", :available=>true}                # <==== accesible via symbol
##########
.

Finished in 0.012338s, 81.0511 runs/s, 81.0511 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

On the contrary, when I launch the integration test (with rack-test), it fails because the nested params are not accessible via symbol but they are stringified as show the output:
spec/web/features_helper.rb

# Require this file for feature tests
require_relative './spec_helper'
require 'rack/test'

class MiniTest::Spec
  include Rack::Test::Methods

  def app
    Hanami.app
  end
end

spec/web/features/items/create_spec.rb

require 'features_helper'

describe 'POST /items api' do

  after do
    ItemRepository.new.clear
  end

  it 'responds with 201' do
    header 'Accept', 'application/json'
    header 'Content-Type', 'application/json'
    post '/items', JSON.generate({ item: { code: 'ABCD', available: true }})
    assert_equal 201, last_response.status
  end

end

output:

❯ rake test TEST=spec/web/features/items/create_spec.rb   
Run options: --seed 63286

# Running:

##########
#<Web::Controllers::Items::Create::Params:0x007f9d1d51cd10 @env={"rack.version"=>[1, 3], "rack.input"=>#<StringIO:0x007f9d1d5ac028>, "rack.errors"=>#<StringIO:0x007f9d1d5ac0a0>, "rack.multithread"=>true, "rack.multiprocess"=>true, "rack.run_once"=>false, "REQUEST_METHOD"=>"POST", "SERVER_NAME"=>"example.org", "SERVER_PORT"=>"80", "QUERY_STRING"=>"", "PATH_INFO"=>"", "rack.url_scheme"=>"http", "HTTPS"=>"off", "SCRIPT_NAME"=>"/items", "CONTENT_LENGTH"=>"41", "rack.test"=>true, "REMOTE_ADDR"=>"127.0.0.1", "HTTP_ACCEPT"=>"application/json", "CONTENT_TYPE"=>"application/json", "HTTP_HOST"=>"example.org", "HTTP_COOKIE"=>"", "router.request"=>#<HttpRouter::Request:0x007f9d1d51d3a0 @rack_request=#<Rack::Request:0x007f9d1d51d3c8 @params=nil, @env={...}>, @path=[], @extra_env={}, @params=[], @acceptable_methods=#<Set: {}>>, "router.params"=>{:item=>{"code"=>"ABCD", "available"=>true}}, "router.parsed_body"=>{"item"=>{"code"=>"ABCD", "available"=>true}}, "rack.request.query_string"=>"", "rack.request.query_hash"=>{}}, @input={:item=>{"code"=>"ABCD", "available"=>true}}, @result=#<Dry::Validation::Result output={:item=>{:code=>"ABCD", :available=>true}} errors={}>, @params={:item=>{"code"=>"ABCD", "available"=>true}}>

{"code"=>"ABCD", "available"=>true}               # <==== HERE, nested params are stringified
##########
E

Finished in 0.047131s, 21.2176 runs/s, 0.0000 assertions/s.

  1) Error:
POST /items api#test_0001_responds with 201:
Hanami::Model::NotNullConstraintViolationError: SQLite3::ConstraintException: NOT NULL constraint failed: items.code
.
.
.

The error occurs also if I start a server and I make a request with Curl:
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{ "item": { "code": "ABC", "available": "true" }}' "http://localhost:2300/items"

❯ be hanami server
[2017-02-21 23:41:40] INFO  WEBrick 1.3.1
[2017-02-21 23:41:40] INFO  ruby 2.3.1 (2016-04-26) [x86_64-darwin15]
[2017-02-21 23:41:40] INFO  WEBrick::HTTPServer#start: pid=69160 port=2300
##########
#<Web::Controllers::Items::Create::Params:0x007ff8fce77528 @env={"CONTENT_LENGTH"=>"49", "CONTENT_TYPE"=>"application/json", "GATEWAY_INTERFACE"=>"CGI/1.1", "PATH_INFO"=>"", "QUERY_STRING"=>"", "REMOTE_ADDR"=>"::1", "REMOTE_HOST"=>"::1", "REQUEST_METHOD"=>"POST", "REQUEST_URI"=>"http://localhost:2300/items", "SCRIPT_NAME"=>"/items", "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"2300", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26)", "HTTP_HOST"=>"localhost:2300", "HTTP_USER_AGENT"=>"curl/7.43.0", "HTTP_ACCEPT"=>"application/json", "rack.version"=>[1, 3], "rack.input"=>#<Rack::Lint::InputWrapper:0x007ff8fb189dd0 @input=#<StringIO:0x007ff8fb9005f0>>, "rack.errors"=>#<Rack::Lint::ErrorWrapper:0x007ff8fb189da8 @error=#<IO:<STDERR>>>, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "rack.url_scheme"=>"http", "rack.hijack?"=>true, "rack.hijack"=>#<Proc:0x007ff8fb18a0f0@/Users/nico/.gem/ruby/2.3.1/gems/rack-2.0.1/lib/rack/lint.rb:525>, "rack.hijack_io"=>nil, "HTTP_VERSION"=>"HTTP/1.1", "REQUEST_PATH"=>"/items", "router.request"=>#<HttpRouter::Request:0x007ff8fce77be0 @rack_request=#<Rack::Request:0x007ff8fce77c08 @params=nil, @env={...}>, @path=[], @extra_env={}, @params=[], @acceptable_methods=#<Set: {}>>, "router.params"=>{:item=>{"code"=>"ABC", "available"=>"true"}}, "router.parsed_body"=>{"item"=>{"code"=>"ABC", "available"=>"true"}}, "rack.request.query_string"=>"", "rack.request.query_hash"=>{}}, @input={:item=>{"code"=>"ABC", "available"=>"true"}}, @result=#<Dry::Validation::Result output={:item=>{:code=>"ABC", :available=>true}} errors={}>, @params={:item=>{"code"=>"ABC", "available"=>"true"}}>

{"code"=>"ABC", "available"=>"true"}
##########
[example] [INFO] [2017-02-21 23:41:44 +0100] (0.003485s) SELECT `id`, `code`, `available`, `created_at`, `updated_at` FROM `items` LIMIT 1
[example] [ERROR] [2017-02-21 23:41:44 +0100] SQLite3::ConstraintException: NOT NULL constraint failed: items.code: INSERT INTO `items` (`code`, `available`, `created_at`, `updated_at`) VALUES (NULL, NULL, '2017-02-21 22:41:44.370172', '2017-02-21 22:41:44.370172')
Hanami::Model::NotNullConstraintViolationError: SQLite3::ConstraintException: NOT NULL constraint failed: items.code
	/Users/nico/.gem/ruby/2.3.1/gems/hanami-model-1.0.0.beta1/lib/hanami/repository.rb:320:in `rescue in create'
.

As already said, commented out the validation block in the action, both controller and feature tests pass:

❯ rake test TESTOPTS="--verbose"                                                                                                                                     
Run options: --verbose --seed 63683

# Running:

Web::Controllers::Items::Create#test_0001_is successful = ##########
#<Hanami::Action::BaseParams:0x007fed77d46e50 @env={:item=>{:code=>"ABCD", :available=>true}}, @raw={:item=>{:code=>"ABCD", :available=>true}}, @params={:item=>{:code=>"ABCD", :available=>true}}>

{:code=>"ABCD", :available=>true}
##########
0.01 s = .
POST /items api#test_0001_responds with 201 = ##########
#<Hanami::Action::BaseParams:0x007fed7794bee0 @env={"rack.version"=>[1, 3], "rack.input"=>#<StringIO:0x007fed779d3188>, "rack.errors"=>#<StringIO:0x007fed779d3200>, "rack.multithread"=>true, "rack.multiprocess"=>true, "rack.run_once"=>false, "REQUEST_METHOD"=>"POST", "SERVER_NAME"=>"example.org", "SERVER_PORT"=>"80", "QUERY_STRING"=>"", "PATH_INFO"=>"", "rack.url_scheme"=>"http", "HTTPS"=>"off", "SCRIPT_NAME"=>"/items", "CONTENT_LENGTH"=>"41", "rack.test"=>true, "REMOTE_ADDR"=>"127.0.0.1", "HTTP_ACCEPT"=>"application/json", "CONTENT_TYPE"=>"application/json", "HTTP_HOST"=>"example.org", "HTTP_COOKIE"=>"", "router.request"=>#<HttpRouter::Request:0x007fed779684c8 @rack_request=#<Rack::Request:0x007fed779684f0 @params=nil, @env={...}>, @path=[], @extra_env={}, @params=[], @acceptable_methods=#<Set: {}>>, "router.params"=>{:item=>{"code"=>"ABCD", "available"=>true}}, "router.parsed_body"=>{"item"=>{"code"=>"ABCD", "available"=>true}}, "rack.request.query_string"=>"", "rack.request.query_hash"=>{}}, @raw={:item=>{"code"=>"ABCD", "available"=>true}}, @params={:item=>{:code=>"ABCD", :available=>true}}>

{:code=>"ABCD", :available=>true}
##########
0.34 s = .

Finished in 0.357424s, 5.5956 runs/s, 5.5956 assertions/s.

2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

A repo with a sample app to reproduce the behavior could be found here.

I remain available for further clarifications, let me know if I can help.
Thanks again,
Nicolò

Whhops..a PR for fixing this issue was created 5 minutes before the opening of this issue 🐎
hanami/router#141

@nickgnd Thanks for this detailed explanation. Can you confirm that hanami/router#141 fixes the problem?

@jodosha Yep, all works fine and no more red dot 🔴 in my specs 🎉 Thanks!

@nickgnd Thanks for getting back so quickly! 💯