Override method/variable in a gem rails - ruby

I am trying to find a way to override a variable in a rails gem actionpack/http/parameters.rb. I have to process an ndjson stream and the rails middleware cannot process ndjson. It uses ActiveSupport::JSON.decode
This is the source of the code below
DEFAULT_PARSERS = {
Mime[:json].symbol => -> (raw_post) {
data = ActiveSupport::JSON.decode(raw_post)
data.is_a?(Hash) ? data : { _json: data }
}
}
I get this error when it tries to parse ndjson
ActionDispatch::Http::Parameters::ParseError in MyController#activity
My objective is to override the parser to allow it to decode the ndjson... Potentially using split(\n) as opposed to the current ActiveSupport::JSON.decode.
So far i have tried creating a file in lib/ folder and using the following code but it doesn't seem to do the override. How do i do this without Monkey patching
require 'lib/action_dispatch/http/parameters'
module MyParser
module Overrides
extend ActiveSupport::Concern
DEFAULT_PARSERS = {
Mime[:json].symbol => -> (raw_post) {
data = raw_post.split("\n")
data = ActiveSupport::JSON.decode(data)
data.is_a?(Hash) ? data : { _json: data }
}
}
end
end
ActionDispatch::Http::Parameters.include(MyParser::Overrides)
UPDATE:
The second approach i tried:
ActionDispatch::Http::Parameters.const_set(:DEFAULT_PARSERS, {
Mime[:json].symbol => -> (raw_post) {
data = raw_post.split("\n")
data = ActiveSupport::JSON.decode(data)
data.is_a?(Hash) ? data : { _json: data }
},
})
Unfortunately it keeps warning me that the constant is already defined.

I took another approach. Instead of reinitializing the constant, i created a file in config/initializers/custom_params.rb to override the method parse_formatted_parameters (here) that was using the DEFAULT_PARSERS variable. From within i was able to change the Proc value for the json data type.
This conveniently overrides the method and allows the ActionDispatch::Http::Parameters module to pass the ndjson to my controller without any parsing errors.
module ActionDispatch
module Http
module Parameters
extend ActiveSupport::Concern
private
def parse_formatted_parameters(parsers)
parsers[Mime[:json].symbol] = Proc.new { |raw_post|
data = ActiveSupport::JSON.decode(raw_post) rescue nil
if !data
data = raw_post
end
data.is_a?(Hash) ? data : { _json: data }
}
return yield if content_length.zero? || content_mime_type.nil?
# rubocop:enable all
strategy = parsers.fetch(content_mime_type.symbol) { return yield }
begin
strategy.call(raw_post)
rescue # JSON or Ruby code block errors.
my_logger = logger || ActiveSupport::Logger.new($stderr)
my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}"
raise ParseError
end
end
end
end
end

Related

RSpec double/mock instance variable from initializer

I've got a class where in initializer I need to call instance variable from parsed params:
class PrintResults
include SortResults
attr_accessor :views_hash
def initialize(parser)
#parser = parser
#views_hash = parser.page_views
end
I want to test attributes accessors, I tried something below:
RSpec.describe PrintResults do
subject { described_class.new(views_hash) }
describe 'attributes accessors' do
let(:accessors) { double(page_views: { '/that_70s_show' => ['111.111.111.111'] }) }
it 'should have views hash' do
subject.views_hash = accessors
expect(subject.views_hash).to eq(['111.111.111.111'])
end
end
but I'm getting an error:
1) PrintResults attributes accessors should have views hash
Failure/Error: expect(subject.views_hash).to eq(['111.111.111.111'])
expected: ["111.111.111.111"]
got: #<Double (anonymous)>
(compared using ==)
Diff:
## -1 +1 ##
-["111.111.111.111"]
+#<Double (anonymous)>
You assign your test double directly to the attribute that is returned instead of using the initialize method.
Instead of
subject { described_class.new(views_hash) }
describe 'attributes accessors' do
let(:accessors) { double(page_views: { '/that_70s_show' => ['111.111.111.111'] }) }
it 'should have views hash' do
subject.views_hash = accessors
expect(subject.views_hash).to eq(['111.111.111.111'])
end
end
use
subject { described_class.new(parser) }
describe 'attributes accessors' do
let(:parser) { double(page_views: { '/that_70s_show' => ['111.111.111.111'] }) }
it 'should have views hash' do
expect(subject.views_hash).to eq('/that_70s_show' => ['111.111.111.111'])
end
end

Ruby Strong Params: NoMethodError undefined method permit for Integer

My controller:
class V1::SendContractController < V1::BaseController
def create
byebug
bride_membership = Wedding.find(send_params[:weddingId]).bride_memberships[0]
SendBrideContractJob.perform_now(bride_membership, send_params[:contractId])
render json: { enqueuedDelivery: true }, status: :ok
end
private
def send_params
params
.require(:weddingId)
.permit(:contractId)
end
end
My params
Parameters: {"weddingId"=>4, "contractId"=>20, "send_contract"=>{"weddingId"=>4, "contractId"=>20}}
The error
NoMethodError (undefined method `permit' for 4:Integer):
But then when I byebug it I get what I want!
(byebug) params
<ActionController::Parameters {"weddingId"=>4, "contractId"=>20, "controller"=>"v1/send_contract", "action"=>"create", "send_contract"=>{"weddingId"=>4, "contractId"=>20}} permitted: false>
(byebug) params[:weddingId]
4
And I'm using axios with an interceptor to take care of formatting issues:
axios.interceptors.request.use((config) => {
if(config.url !== "/authentications") {
config.paramsSerializer = params => {
// Qs is already included in the Axios package
return qs.stringify(params, {
arrayFormat: "brackets",
encode: false
});
};
axios.defaults.headers.common['Authorization'] = `Bearer ${store.state.token.token}`
config.headers.common['Authorization']= `Bearer ${store.state.token.token}`
axios.defaults.headers.common['Accept'] = 'application/vnd.bella.v1+json'
config.headers.common['Accept'] = 'application/vnd.bella.v1+json'
return config
}
return config
})
I believe that require gives you the object at the key you provide to do further permit and / or require calls.
Perhaps you could try (not tested):
params.require(:weddingId)
params.permit(:weddingId, :contractId)
Edit: there's this too: Multiple require & permit strong parameters rails 4
Refer to this documentation and question.The require ensures that a parameter is present. If it's present, returns the parameter at the given key, otherwise raises an ActionController::ParameterMissing error.
p = { "weddingId"=>4, "contractId"=>20 }
ActionController::Parameters.new(p).require(:weddingId)
# 4
p = { "weddingId"=>nil, "contractId"=>20 }
ActionController::Parameters.new(p).require(:weddingId)
# ActionController::ParameterMissing: param is missing or the value is empty: weddingId
If you want to make sure :weddingId is present:
def contract_params
params.require(:weddingId)
params.permit(:contractId, :weddingId)
end
BTW, SendContractController is better called ContractsController.

Ruby stubbing with faraday, can't get it to work

Sorry for the title, I'm too frustrated to come up with anything better right now.
I have a class, Judge, which has a method #stats. This stats method is supposed to send a GET request to an api and get some data as response. I'm trying to test this and stub the stats method so that I don't perform an actual request. This is what my test looks like:
describe Judge do
describe '.stats' do
context 'when success' do
subject { Judge.stats }
it 'returns stats' do
allow(Faraday).to receive(:get).and_return('some data')
expect(subject.status).to eq 200
expect(subject).to be_success
end
end
end
end
This is the class I'm testing:
class Judge
def self.stats
Faraday.get "some-domain-dot-com/stats"
end
end
This currently gives me the error: Faraday does not implement: get
So How do you stub this with faraday? I have seen methods like:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get('http://stats-api.com') { [200, {}, 'Lorem ipsum'] }
end
But I can't seem to apply it the right way. What am I missing here?
Note that Faraday.new returns an instance of Faraday::Connection, not Faraday. So you can try using
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return("some data")
Note that I don't know if returning "some data" as shown in your question is correct, because Faraday::Connection.get should return a response object, which would include the body and status code instead of a string. You might try something like this:
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double("response", status: 200, body: "some data")
)
Here's a rails console that shows the class you get back from Faraday.new
$ rails c
Loading development environment (Rails 4.1.5)
2.1.2 :001 > fara = Faraday.new
=> #<Faraday::Connection:0x0000010abcdd28 #parallel_manager=nil, #headers={"User-Agent"=>"Faraday v0.9.1"}, #params={}, #options=#<Faraday::RequestOptions (empty)>, #ssl=#<Faraday::SSLOptions (empty)>, #default_parallel_manager=nil, #builder=#<Faraday::RackBuilder:0x0000010abcd990 #handlers=[Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]>, #url_prefix=#<URI::HTTP:0x0000010abcd378 URL:http:/>, #proxy=nil>
2.1.2 :002 > fara.class
=> Faraday::Connection
Coming to this late, but incase anyone else is too, this is what worked for me - a combination of the approaches above:
let(:json_data) { File.read Rails.root.join("..", "fixtures", "ror", "501100000267.json") }
before do
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double(Faraday::Response, status: 200, body: json_data, success?: true)
)
end
Faraday the class has no get method, only the instance does. Since you are using this in a class method what you can do is something like this:
class Judge
def self.stats
connection.get "some-domain-dot-com/stats"
end
def self.connection=(val)
#connection = val
end
def self.connection
#connection ||= Faraday.new(some stuff to build up connection)
end
end
Then in your test you can just set up a double:
let(:connection) { double :connection, get: nil }
before do
allow(connection).to receive(:get).with("some-domain-dot-com/stats").and_return('some data')
Judge.connection = connection
end
I ran into the same problem with Faraday::Adapter::Test::Stubs erroring with Faraday does not implement: get. It seems you need to set stubs to a Faraday adapter, like so:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get("some-domain-dot-com/stats") { |env| [200, {}, 'egg'] }
end
test = Faraday.new do |builder|
builder.adapter :test, stubs
end
allow(Faraday).to receive(:new).and_return(test)
expect(Judge.stats.body).to eq "egg"
expect(Judge.stats.status).to eq 200
A better way to do this, rather than using allow_any_instance_of, is to set the default connection for Faraday, so that Faraday.get will use the connection you setup in your tests.
For example:
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } }
before do
stubs.get('/maps/api/place/details/json') do |_env|
[
200,
{ 'Content-Type': 'application/json' },
{ 'result' => { 'photos' => [] } }.to_json
]
end
Faraday.default_connection = conn
end
after do
Faraday.default_connection = nil
end

Rails pagination in API using Her, Faraday

I've been trying to figure this out all day, and it's driving me crazy.
I have two rails apps, ServerApp and ClientApp. ClientApp gets data from ServerApp through an API, using the Her gem. Everything was great until I needed pagination information.
This is the method I am using to get the orders (this uses kamainari for pagination and ransack for search):
# ServerApp
def search
#search = Order.includes(:documents, :client).order('id desc').search(params[:q])
#orders = #search.result(distinct: true).page(params[:page]).per(params[:per])
respond_with #orders.as_json(include: :documents)
end
It returns an array of hashes in json, which Her uses as a collection of orders. That works fine.
# Response
[
{
"client_id": 239,
"created_at": "2013-05-15T15:37:03-07:00",
"id": 2422,
"ordered_at": "2013-05-15T15:37:03-07:00",
"origin": "online",
"updated_at": "2013-05-15T15:37:03-07:00",
"documents": [
{ ... }
]
},
...
]
But I needed pagination information. It looked like I needed to send it as metadata with my json. So I change my response to this:
respond_to do |format|
format.json do
render json: { orders: #orders.as_json(include: :documents), metadata: 'sent' }
end
end
This does indeed send over metadata, so in my ClientApp I can write #orders.metadata and get 'sent'. But now my orders are nested in an array inside of 'orders', so I need to use #orders.orders, and then it treats it like an array instead of a Her collection.
After doing some reading, it seemed sending pagination info through headers was the way a lot of other people did this (I was able to get the headers set up in an after_filter using this guide). But I am even more lost on how to get those response headers in my ClientApp - I believe I need a Faraday Middleware but I just am having no luck getting this to work.
If anyone knows how I can just get this done, I would be very grateful. I can't take another day of banging my head against the wall on this, but I feel like I am just one vital piece of info away from solving this!
I encountered the same issue and solved it by adding my own middleware and rewriting the "parse" and "on_complete" methods without that much hassle and avoiding the use of global variables.
Here's the code:
class CustomParserMiddleware < Her::Middleware::DefaultParseJSON
def parse(env)
json = parse_json(env[:body])
pagination = parse_json(env[:response_headers][:pagination_key]) || {}
errors = json.delete(:errors) || {}
metadata = json.delete(:metadata) || {}
{
:data => json,
:errors => errors,
:metadata => {
:pagination => pagination,
:additional_metadata => metadata
},
end
def on_complete(env)
env[:body] = case env[:status]
when 204
parse('{}')
else
parse(env)
end
end
end
then, you can access the pagination as follows:
model = Model.all
model.metadata[:pagination]
I finally got this working. The trick was to use a global variable in the faraday on_complete - I tried to find a better solution but this was the best I could do. Once again, I got the header code from here. Here's the full guide to how to get pagination working with Her:
First, on my server side, I have the Kaminari gem, and I pass page and per as params to the server from the client. (This is also using ransack for searching)
def search
#search = Order.order('id desc').search(params[:q])
#orders = #search.result(distinct: true).page(params[:page]).per(params[:per])
respond_with #orders.as_json(include: :items)
end
My client makes the request like so:
#orders = Order.search(q: { client_id_eq: #current_user.id }, page: params[:page], per: 3)`
Back on the server, I have this in my ApiController (app controller for api):
protected
def self.set_pagination_headers(name, options = {})
after_filter(options) do |controller|
results = instance_variable_get("##{name}")
headers["X-Pagination"] = {
total_count: results.total_count,
offset_value: results.offset_value
}.to_json
end
end
In the server orders_controller.rb, I set the pagination headers for the search method:
class OrdersController < ApiController
set_pagination_headers :orders, only: [:search]
...
end
Now to receive the headers we need a Faraday middleware in Her on the client.
# config/initializers/her.rb
Her::API.setup url: Constants.api.url do |c|
c.use TokenAuthentication
c.use HeaderParser # <= This is my middleware for headers
c.use Faraday::Request::UrlEncoded
c.use Her::Middleware::DefaultParseJSON
c.use Faraday::Adapter::NetHttp
c.use Faraday::Response::RaiseError
end
# lib/header_parser.rb
# don't forget to load this file in application.rb with something like:
# config.autoload_paths += Dir[File.join(Rails.root, "lib", "*.rb")].each { |l| require l }
class HeaderParser < Faraday::Response::Middleware
def on_complete(env)
unless env[:response_headers]['x-pagination'].nil?
# Set the global var for pagination
$pagination = JSON.parse(env[:response_headers]['x-pagination'], symbolize_names: true)
end
end
end
Now back in your client controller, you have a global variable of hash called $pagination; mine looks like this:
$pagintation = { total_count: 0, offset_value: 0 }`
Finally, I added Kaminari gem to my client app to paginate the array and get those easy pagination links:
#orders = Kaminari.paginate_array(#orders, total_count: $pagination[:total_count]).page(params[:page]).per(params[:per_page])`
I hope this can help someone else, and if anyone knows a better way to do this, let me know!
You can pass header options to Faraday when setting up the connection, see the docs at http://rubydoc.info/gems/faraday/0.8.7/Faraday/Connection:initialize
Sometimes it helps to do a curl request first, esp. use -vv option for verbose output where you will see all headers. (Maybe you can attach some log outputs from the Server too)
You can use e.g. clogger (http://clogger.rubyforge.org/) do monitor header information on the Rails server side

Possible to autorender in Ruby Espresso framework?

I really like the way Ramaze auto renders templates without you having to call a render function. Is it possible to have the same in Espresso?
Yep, i liked it too when were using Ramaze.
However, with Espresso, there are multiple reasons to NOT do this automatically.
Instead of listing them, i'll post a easy way of achieving this.
You simply need to set an after hook for controllers that will have autorender enabled.
You can set the hook inside each controller or for all at once when you build your app:
E.new do
setup_controllers do
after { response.body = [ render ] }
end
# ...
end
You can of course set it for specific controllers only:
E.new do
setup_controllers do
if self == Foo || self == Bar
after { response.body = [ render ] }
end
end
# ...
end
as well as for specific actions:
E.new do
setup_controllers do
if self == Foo
after(:index) { response.body = [ render ] }
elsif self == Bar
after(/blah/) { response.body = [ render ] }
else
after { response.body = [ render ] }
end
end
# ...
end
note: posting a bit hairy code for demonstration purposes, optimize it as required by your application.

Resources