I have a Controller:
class ThingController < ActionController
respond_to :json
def create
puts "CREATE " + params.inspect
end
end
and a test:
require "spec_helper"
describe "/thing" do
context "create" do
it "should get params" do
params = {"a" => "b", "c" => ["d"], "e" => [], "f"=>"",
"g"=>nil, , "controller" => "NOPE", "action" => "NOPE"}
post uri, params
end
end
end
When I run this, the following is logged:
CREATE {"a"=>"b", "c"=>["d"], "action"=>"create", "controller"=>"thing"}
My questions are:
where did e go? I would expect it to deserialize to an empty array, not to nothing at all.
why are the action and controller params being mixed into this? Aren't the body and the rails internals completely separate concerns?
Because of this, my action and controller JSON fields were over-written. How would I access these?
Is this therefore not the right way to accept JSON?
I'm new to Rails, but I have done a lot of Django.
There are two parts to this problem: you need to ensure that your parameters are being sent as JSON, and also that they are being interpreted as JSON.
Essentially, you have to
encode your parameters as JSON
set appropriate content-type and accepts headers
See POSTing raw JSON data with Rails 3.2.11 and RSpec for the way.
The rails middleware will add the action and controller params so you'll have to put those in a nested hash if you still want to access your custom values.
Try adding format: 'json' to the params in your test. This will send a different content-type header and might help serialize the params correctly in order to keep the e param.
Related
Disclaimer: I am very new to Ruby + rails. I'm not sure if this is a bug, but my params variable always seems to be null. I am working on a large and unfamiliar codebase so I'm not sure if it's something else interfering or my own code; any suggestions would be welcome however.
In my routes file I have match '/proxy_request/:number/:ref' => 'proxies#show', via: :get- I was under the impression that this would store :number and :ref variables in params. However when my proxies#show function runs (below), params is an empty hash.
In case it probably is something else interfering with params, is there another way to pass :number and :ref to proxies#show?
class ProxiesController < ApplicationController
include Service
skip_before_action :restrict_access!
def show
binding.pry #params is null here
data = { date: Adapter.staging_date.get(params[:number], params[:ref])}
render json: data, content_type: "application/javascript", callback: #_request.env["QUERY_STRING"].match(/jQuery\d*_\d*/)
end
end
I removed the include Service and all seems to be ok now
I have a Backbone model in my app which is not a typical flat object, it's a large nested object and we store the nested parts in TEXT columns in a MySQL database.
I wanted to handle the JSON encoding/decoding in Rails API so that from outside it looks like you can POST/GET this one large nested JSON object even if parts of it are stored as stringified JSON text.
However, I ran into an issue where Rails magically converts empty arrays to nil values. For example, if I POST this:
{
name: "foo",
surname: "bar",
nested_json: {
complicated: []
}
}
My Rails controller sees this:
{
:name => "foo",
:surname => "bar",
:nested_json => {
:complicated => nil
}
}
And so my JSON data has been altered..
Has anyone run into this issue before? Why would Rails be modifying my POST data?
UPDATE
Here is where they do it:
https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/request.rb#L288
And here is ~why they do it:
https://github.com/rails/rails/pull/8862
So now the question is, how to best deal with this in my nested JSON API situation?
After much searching, I discovered that you starting in Rails 4.1 you can skip the deep_munge "feature" completely using
config.action_dispatch.perform_deep_munge = false
I could not find any documentation, but you can view the introduction of this option here:
https://github.com/rails/rails/commit/e8572cf2f94872d81e7145da31d55c6e1b074247
There is a possible security risk in doing so, documented here: https://groups.google.com/forum/#!topic/rubyonrails-security/t1WFuuQyavI
Looks like this is a known, recently introduced issue: https://github.com/rails/rails/issues/8832
If you know where the empty array will be you could always params[:...][:...] ||= [] in a before filter.
Alternatively you could modify your BackBone model's to JSON method, explicitly stringifying the nested_json value using JSON.stringify() before it gets posted and manually parsing it back out using JSON.parse in a before_filter.
Ugly, but it'll work.
You can re-parse the parameters on your own, like this:
class ApiController
before_filter :fix_json_params # Rails 4 or earlier
# before_action :fix_json_params # Rails 5
[...]
protected
def fix_json_params
if request.content_type == "application/json"
#reparsed_params = JSON.parse(request.body.string).with_indifferent_access
end
end
private
def params
#reparsed_params || super
end
end
This works by looking for requests with a JSON content-type, re-parsing the request body, and then intercepting the params method to return the re-parsed parameters if they exist.
I ran into similar issue.
Fixed it by sending empty string as part of the array.
So ideally your params should like
{
name: "foo",
surname: "bar",
nested_json: {
complicated: [""]
}
}
So instead of sending empty array I always pass ("") in my request to bypass the deep munging process.
Here's (I believe) a reasonable solution that does not involve re-parsing the raw request body. This might not work if your client is POSTing form data but in my case I'm POSTing JSON.
in application_controller.rb:
# replace nil child params with empty list so updates occur correctly
def fix_empty_child_params resource, attrs
attrs.each do |attr|
params[resource][attr] = [] if params[resource].include? attr and params[resource][attr].nil?
end
end
Then in your controller....
before_action :fix_empty_child_params, only: [:update]
def fix_empty_child_params
super :user, [:child_ids, :foobar_ids]
end
I ran into this and in my situation, if a POSTed resource contains either child_ids: [] or child_ids: nil I want that update to mean "remove all children." If the client intends not to update the child_ids list then it should not be sent in the POST body, in which case params[:resource].include? attr will be false and the request params will be unaltered.
I ran into a similar issue and found out that passing an array with an empty string would be processed correctly by Rails, as mentioned above.
If you encounter this while submitting a form, you might want to include an empty hidden field that matches the array param :
<input type="hidden" name="model[attribute_ids][]"/>
When the actual param is empty the controller will always see an array with an empty string, thus keeping the submission stateless.
I'm looking for a clean way to use JBuilder and test the json output with RSpec. The popular way for JSON testing is to implement the as_json method, and then in RSpec compare the received object with the object.to_json method. But a large reason I'm using JBuilder is that I don't want all the attributes that to_json spits out; so this breaks comparison.
Currently with JBuilder I'm having to do the following to test the RSpec results:
1) Create a Factory object: #venue
2) Create a hash inside my RSpec test that contains the "expected" JSON string back
#expected => {:id => #venue.id,:name=>#venue.name..........}
2) Compare the #expected string to the results.response.body that is returned from the JSON call.
This seems simple, except I have objects being rendered with 15+ attributes, and building the #expected hash string is cumbersome and very brittle. Is there a better way to do this?
You should be able to test your Jbuilder views with RSpec views specs. You can see the docs at https://www.relishapp.com/rspec/rspec-rails/v/2-13/docs/view-specs/view-spec.
An example spec for a file located at 'app/views/api/users/_user.json.jbuilder', could be something like this (spec/views/api/users/_user.json.jbuilder_spec.rb):
require 'spec_helper'
describe 'user rendering' do
let(:current_user) { User.new(id: 1, email: 'foo#bar.com') }
before do
view.stub(:current_user).and_return(current_user)
end
it 'does something' do
render 'api/users/user', user: current_user
expect(rendered).to match('foo#bar.com')
end
end
I don't like testing the JSON API through the views, because you have to essentially mimic, in the test, the setup already done in the controller. Also, bypassing the controller, you aren't really testing the API.
In controller tests, however, you'll find that you don't get any JSON returned in the response body. The response body is empty. This is because RSpec disables view rendering in controller tests. (For better or worse.)
In order to have an RSpec controller test of your view rendered JSON API, you must add the render_views directive at the top of your test. See this blog post (not mine), for more detailed information about using RSpec controller tests with Jbuilder.
Also, see this answer.
I have not been able to make RSpec work with the views yet, but I am testing my JSON API via controller RSpec tests. To assist with this process, I am using the api matchers gem. This gem lets you construct RSpec tests such as:
it "should have a 200 code" do
get :list, :format => :json
response.should be_success
response.body.should have_json_node( :code ).with( "200" )
response.body.should have_json_node( :msg ).with( "parameter missing" )
end
This sounds like a good use case for RSpec view specs. Are you using JBuilder for the output of a controller in views?
For example, in spec/views/venues_spec.rb
require 'spec_helper'
describe "venues/show" do
it "renders venue json" do
venue = FactoryGirl.create(:venue)
assign(:venue, venue])
render
expect(view).to render_template(:partial => "_venue")
venue_hash = JSON.parse(rendered)
venue_hash['id'].should == #venue.id
end
end
It's a little clunkier than with say ERB, but you can use binding and eval to run the Jbuilder template directly. E.g. given a typical Jbuilder template app/views/items/_item.json.jbuilder that refers to an instance item of the Item model:
json.extract! item, :id, :name, :active, :created_at, :updated_at
json.url item_url(item, format: :json)
Say you have an endpoint that returns a single Item object. In your request spec, you hit that endpoint:
get item_url(id: 1), as: :json
expect(response).to be_successful # just to be sure
To get the expected value, you can evaluate the template as follows:
item = Item.find(1) # local variable `item` needed by template
json = JbuilderTemplate.new(JbuilderHandler) # local variable `json`, ditto
template_path = 'app/views/items/_item.json.jbuilder'
binding.eval(File.read(template_path)) # run the template
# or, for better error messages:
# binding.eval(File.read(template_path), File.basename(template_path))
expected_json = json.target! # template result, as a string
Then you can compare the template output to your raw HTTP response:
expect(response.body).to eq(expected_json) # plain string comparison
Or, of course, you can parse and compare the parsed results:
actual_value = JSON.parse(response.body)
expected_value = JSON.parse(expected_json)
expect(actual_value).to eq(expected_value)
If you're going to be doing this a lot -- or if, for instance, you want to be able to compare the template result against individual elements of a returned JSON array, you might want to extract a method:
def template_result(template_path, bind)
json = JbuilderTemplate.new(JbuilderHandler)
# `bind` is passed in and doesn't include locals declared here,
# so we need to add `json` explicitly
bind.local_variable_set(:json, json)
bind.eval(File.read(template_path), File.basename(template_path))
JSON.parse(json.target!)
end
You can then do things like:
it 'sorts by name by default' do
get items_url, as: :json
expect(response).to be_successful
parsed_response = JSON.parse(response.body)
expect(parsed_response.size).to eq(Item.count)
expected_items = Item.order(:name)
expected_items.each_with_index do |item, i| # item is used by `binding`
expected_json = template_result('app/views/items/_item.json.jbuilder', binding)
expect(parsed_response[i]).to eq(expected_json)
end
end
You can call the render function directly.
This was key for me to get local variables to work.
require "rails_helper"
RSpec.describe "api/r2/meditations/_meditation", type: :view do
it "renders json" do
meditation = create(:meditation)
render partial: "api/r2/meditations/meditation", locals: {meditation: meditation}
meditation_hash = JSON.parse(rendered)
expect(meditation_hash['slug']).to eq meditation.slug
expect(meditation_hash['description']).to eq meditation.description
end
end
Is there a neat way in rails to get a hash of the params without the default ones of 'action' and 'controller'? Essentially without any param that wasn't added by me.
I've settled for:
parm = params.clone
parm.delete('action')
parm.delete('controller');
But wondering if there is a neater way to do this?
You could use except:
params.except(:action, :controller)
http://as.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Hash/Except.html
request.path_parameters
returns path_parameters
request.query_parameters
returns request_parameters
You are looking for the latter.
If you are working in a controller, you should also have access to the request object.
To make a long story short, rails and rack groom incoming GET/POST requests (form, xml, json) and pull out the parameters so that developers have a consistent way of accessing them.
ActionDispatch exposes the consolidated list of params via:
# ActionPack 3.1.8 - action_dispatch/http/parameters.rb
# Returns both GET and POST \parameters in a single hash.
def parameters
#env["action_dispatch.request.parameters"] ||= begin
params = request_parameters.merge(query_parameters)
params.merge!(path_parameters)
encode_params(params).with_indifferent_access
end
end
alias :params :parameters
As you can see, params is an alias for the parameters method which is a merged hash of two sub-hashes: request_parameters and path_parameters.
In your case, you don't want the path_parameters. Rather than using except, which forces you to know which path parameters you want to exclude, you can access your data via: request.request_parameters.
A word of caution: You may be better off using :except if you require the hash to be encoded and keys to be accessed as either strings or symbols. The last line of the parameters method handles that for you:
encode_params(params).with_indifferent_access
An alternative approach using except and ensuring that you are removing all rails non-request parameters:
path_params = request.path_parameters
params.except(*path_params.keys)
use
request.request_parameters
it excludes the path_parameters (controller and action)
I use
request.request_parameters.except(controller_name.singularize)
This strips out the nested object that is named after the active controller. For example with the following controller:
Class SessionController > ActionController::Base
def create
User.find_by(params[:email]).login(password: params[:password])
puts request.request_parameters
end
end
With the following posted value from a web form:
{email: 'test#example.com', password: 'password123'}
The console output will be:
{"email"=>"test#example.com", "password"=>"password123", "session"=>{"email"=>"test#example.com", "password"=>"password123"}}
The above lines of code avoid this.
By default calling rails.model.to_json
Will display something like this:
{"name":["can't be blank"],"email":["can't be blank"],"phone":["can't be blank"]}
Instead of message i need to generate some status code that could be used on service client:
[{"field": "name", "code": "blank"}, {"field": "email", "code": "blank"}]
This approach is very similar to github api v3 errors - http://developer.github.com/v3/
How can I achieve this with Rails?
On your model you can modify the way as json operates. For instance let us assume you have a ActiveRecord model Contact. You can override as_json to modify the rendering behavior.
def Contact < ActiveRecord::Base
def as_json
hash = super
hash.collect {|key, value|
{"field" => key, "code" => determine_code_from(value)}
}
end
end
Of course, you could also generate the json in a separate method on Contact or even in the controller. You would just have to alter your render method slightly.
render #contact.as_my_custom_json
In your controller, when you render the output, in your case JSON content, add the following :
render :json => #yourobject, :status => 422 # or whatever status you want.
Hope this helps