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
Related
I'm trying to learn processing JSON with Ruby. I went through quite a few tutorial but got confused even more, so I try to learn by doing it.
Here, I'm trying to get user's data from Instagram and display in my View. I have access to the JSON below but how do I reach certain fields and username or loop posts?
# users_controller.rb
require 'httparty'
class UsersController < ApplicationController
include HTTParty
def show
#user = User.find(params[:id])
fetch_instagram("elonofficiall")
end
private
def fetch_instagram(instagram_username)
url = "https://www.instagram.com/#{instagram_username}/?__a=1"
#data = HTTParty.get(url)
return #data
end
end
# show.html.erb
# the code below is just to try if I get anything
<%= #data %>
<% #data.each do |data| %>
<p><%= data %></p>
<% end %>
https://www.instagram.com/elonofficiall/?__a=1
First off don't do HTTP calls straight from your controller.
Instead create a separate class that "talks" to the instagram API:
# app/clients/instagram_client.rb
class InstagramClient
include HTTParty
base_uri 'https://www.instagram.com'
format :json
def initialize(**options)
#options = options
end
def user(username, **opts)
options = #options.reverse_merge(
'__a' => 1 # wtf is this param?
).reverse_merge(opts)
response = self.class.get("/#{username}", options)
if response.success?
extract_user(response)
else
Rails.logger.error("Fetching Instagram feed failed: HTTP #{response.code}")
nil
end
end
private
def extract_user(json)
attributes = response.dig('graphql', 'user')&.slice('id', 'biography')
attributes ? InstagramUser.new(attributes) : nil
end
end
And an class that normalizes the API response for consumption in your application:
# app/models/instagram_user.rb
class InstagramUser
include ActiveModel::Model
include ActiveModel::Attributes
attribute :id
attribute :biography
attribute :username
# etc
end
This is just a straight up Rails model that is not persisted in the database. ActiveModel::Model and ActiveModel::Attributes let you pass a hash of attributes for mass assignment just like you do with ActiveRecord backed models.
This gives you an object you can simply test by:
InstagramClient.new.feed('elonofficiall')
And you would integrate it into your controller like so:
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
fetch_instagram(#user.instagram_username)
end
private
def fetch_instagram(instagram_username)
#instagram_user = InstagramClient.new.user(#user.instagram_username)
end
end
Mixing HTTParty into your controller is a straight up bad idea as controllers are tricky to test and the controller already has the responsibility of responding to client requests and does not need more.
Also processing a "raw" JSON response in your view is not a great practice as it creates a hard coupling between the external API and your application and adds far to much complexity to your views. Normalize the data first in a separate object.
<% if #instagram_user %>
<p><%= #instagram_user.username %></p>
<% end %>
If you want to actually get the media items from the users instagram feed you need to make another HTTP request to GET https://graph.facebook.com/{ig-user-id}/media. See the official API documentation.
Best to take it out of Rails first off. That only convolutes things. Many devs reply Rails === Ruby. It is not.
Write a simple ruby script and test her out:
rec = HTTParty.get(url)
puts rec.headers # content-type is 'text/html' and should be 'application/json'
res = rec.parsed_response # getting error
puts res.class
The issue with Instagram is that I think it's forcing a bad content-header back to the user. It might be a security layer. I can't seem to force the reply into json or a hash either. So that's why httparty is not working for me.
I have used Ruby on Rails to create a simple Rest api server. I have added to my route.rb this, which sends the whole path to myapp's controller's update method, which I need:
put 'dreceiver/*other', to: 'myapps#update'
My controller:
class MyAppsController < ApplicationController
protect_from_forgery with: :null_session
# PUT
# Expected Parameters: {"other"=>"api/1.0/file/abc123"}
def update
#Need to grab last part of path sent in:
if params[:other] =~ /api\/1.0\/file\/.*/
batchid = params[:other].split('/').last
else
batchid = nil
end
unless batchid.nil?
render :text => '', :status => 201
else
render :text => '', :status => 401
end
return
end
end
My problem: I expect a gzip file to be sent in the body of the PUT request. I need to save it to the file system. I see lot's of examples of storing it in the db using paperclip, but I really don't want to do any migrations or deal with the db if I don't have to... this is just to test some client code sending the file. Thanks for your help in advance.
Paperclip does not store uploaded files in the database, it stores them on the filesystem and then writes the filename to the database (usually -- it can be configured in multiple ways).
Multipart POSTs (and PUTs or PATCHes) work just like regular POSTs. Rails will automatically create a Hash-like object from the request body. One of the values in this hash should be a object that responds to #tempfile that you can treat pretty much just like a normal File object.
def update
gzip_file = params[:uploaded_file].tempfile # replace uploaded_file with the field name
...
end
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.
I've got some data in Rails that I want to render as JSON data. What I'm doing right now is simply finding all instances of a Model and calling render :json=>data.
data = Data.find(:all)
render :json => data
However, Rails is including the model name in each JSON object. So my JSON data ends up looking like this:
[{modelname:{propertyName: 'value',...}},{modelname:{propertyName: 'value2',...}}]
instead of this:
[{propertyName:'value',...},{propertyName:'value2',...}]
The modelname is always the same and I don't want it to be there.
I changed the option to render the root in the JSON data in one of the Rails initializers but that affects everything that I want rendered as JSON, which I don't want to do for this project.
In this case, I want to be able to do this on a case-by-case basis.
How can I do this? Thanks in advance.
With Rails 3, you can use active_model_serializers gem1
that allows you to specify rootless rendering of an object like this:
render :json => data, :root => false
I did not find a way to do this by passing options to the to_json method (and I don't believe there is such an option). You have more alternative to do this, any class that inherits from ActiveRecord::Base will have include_root_in_json.
Do something like this.
Data.include_root_in_json = false
data = Data.find(:all)
render :json => data
Hope this gets you going.
Ok let's try this then.
DataController < ApplicationControlle
private
def custom_json(data)
Data.include_root_in_json = false
data.to_json
Data.include_root_in_json = true
data
end
end
Then your redirect would look like this
data = Data.find(:all)
render :json => custom_json(data)
It's pretty silly code I wish I could think of something else entirely. Let me ask you this: What is it about having the model name included in the json data ?
With Rails 3, I found this way better to do. Override the as_json in your model and do as follows:
def as_json(options = {})
super(options.merge :methods => [:some_method_that_you_want_to_include_result], :include => {:child_relation => {:include => :grand_child_relation } })
end
Update:
This issue was not properly explored. The real issue lies within render :json.
The first code paste in the original question will yield the expected result. However, there is still a caveat. See this example:
render :json => current_user
is NOT the same as
render :json => current_user.to_json
That is, render :json will not automatically call the to_json method associated with the User object. In fact, if to_json is being overridden on the User model, render :json => #user will generate the ArgumentError described below.
summary
# works if User#to_json is not overridden
render :json => current_user
# If User#to_json is overridden, User requires explicit call
render :json => current_user.to_json
This all seems silly to me. This seems to be telling me that render is not actually calling Model#to_json when type :json is specified. Can someone explain what's really going on here?
Any genii that can help me with this can likely answer my other question: How to build a JSON response by combining #foo.to_json(options) and #bars.to_json(options) in Rails
Original Question:
I've seen some other examples on SO, but I none do what I'm looking for.
I'm trying:
class User < ActiveRecord::Base
# this actually works! (see update summary above)
def to_json
super(:only => :username, :methods => [:foo, :bar])
end
end
I'm getting ArgumentError: wrong number of arguments (1 for 0) in
/usr/lib/ruby/gems/1.9.1/gems/activesupport-2.3.5/lib/active_support/json/encoders/object.rb:4:in `to_json
Any ideas?
You are getting ArgumentError: wrong number of arguments (1 for 0) because to_json needs to be overridden with one parameter, the options hash.
def to_json(options)
...
end
Longer explanation of to_json, as_json, and rendering:
In ActiveSupport 2.3.3, as_json was added to address issues like the one you have encountered. The creation of the json should be separate from the rendering of the json.
Now, anytime to_json is called on an object, as_json is invoked to create the data structure, and then that hash is encoded as a JSON string using ActiveSupport::json.encode. This happens for all types: object, numeric, date, string, etc (see the ActiveSupport code).
ActiveRecord objects behave the same way. There is a default as_json implementation that creates a hash that includes all the model's attributes. You should override as_json in your Model to create the JSON structure you want. as_json, just like the old to_json, takes an option hash where you can specify attributes and methods to include declaratively.
def as_json(options)
# this example ignores the user's options
super(:only => [:email, :handle])
end
In your controller, render :json => o can accept a string or an object. If it's a string, it's passed through as the response body, if it's an object, to_json is called, which triggers as_json as explained above.
So, as long as your models are properly represented with as_json overrides (or not), your controller code to display one model should look like this:
format.json { render :json => #user }
The moral of the story is: Avoid calling to_json directly, allow render to do that for you. If you need to tweak the JSON output, call as_json.
format.json { render :json =>
#user.as_json(:only => [:username], :methods => [:avatar]) }
If you're having issues with this in Rails 3, override serializable_hash instead of as_json. This will get your XML formatting for free too :)
For people who don't want to ignore users options but also add their's:
def as_json(options)
# this example DOES NOT ignore the user's options
super({:only => [:email, :handle]}.merge(options))
end
Hope this helps anyone :)
Override not to_json, but as_json.
And from as_json call what you want:
Try this:
def as_json
{ :username => username, :foo => foo, :bar => bar }
end