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?
@nickgnd Thanks for getting back so quickly! 💯