Possible to autorender in Ruby Espresso framework? - ruby

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.

Related

Override method/variable in a gem rails

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

ruby rackup: is it possible to programatically add a mapping from within another mapping?

I have a .ru file and can set up mappings without issue ( the 'register' mapping below ).
However I want services to be able to register themselves by hitting a url so I want to be able to add new mappings on the fly from within others mappings.
The below code does not work though. What am I doing wrong and is this possible?
Thanks!
map '/register' do
run Proc.new { |env|
# inside of register i want to add another mapping.
# obviously 'bar' would be a value read out of env
map '/bar' do
run Proc.new{ |env| ['200', { 'Content-Type' => 'text/html' },'bar' }
end
[ '200', {'Content-Type' => 'text/html'}, "registered"]
}
end
I don't think there's a way to add routes after-the-fact using map. One alternative is to use Rack::URLMap to define your app. You'll need to maintain your own list of registered routes (as a hash) and call Rack::URLMap#remap every time you add a new route to the hash:
url_map = Rack::URLMap.new
routes = {
"/register" => lambda do |env|
routes["/bar"] = lambda do |env|
[ "200", {"Content-Type" => "text/plain"}, ["bar"] ]
end
url_map.remap(routes)
[ "200", {"Content-Type" => "text/plain"}, ["registered"] ]
end
}
url_map.remap(routes)
run url_map
Note that you could do with with just the hash, but URLMap provides some nice conveniences, including 404 handling. It's actually a really nice little class and worth reading if you have five minutes to spare.
If you were so inclined, you could turn this into a tidy little class:
class Application
def initialize
#routes = {}
#url_map = Rack::URLMap.new
register_route "/register" do |env|
# When "/register" is requested, register the new route "/bar"
register_route "/bar" do |env|
[ 200, {"Content-Type" => "text/plain"}, ["bar"] ]
end
[ 200, {"Content-Type" => "text/plain"}, ["registered"] ]
end
end
def call(env)
#url_map.call(env)
end
private
def register_route(path, &block)
#routes[path] = block
#url_map.remap(#routes)
end
end
run Application.new
According to https://rack.github.io/, "To use Rack, provide an "app": an object that responds to the call method, taking the environment hash as a parameter, and returning an Array with three elements:
The HTTP response code
A Hash of headers
The response body, which must respond to each"
Your third element will not respond to each. Maybe wrap it in an array?

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

Why is my rspec test doubling my objects?

This is one of those cases where my code is working but my test is failing and I need to know what I am doing wrong?
I have a Project class with an all method that just spits out instances of this class:
class Project
##all_projects = []
def initialize(options)
##all_projects << self
end
def self.all
##all_projects
end
end
Now Project.all works just fine but the spec I am writing doesn't.
context "manipulating projects" do
before do
options1 = {
name: 'Building house'
}
options2 = {
name: 'Getting a loan from the Bank'
}
#project1 = Project.new(options1)
#project2 = Project.new(options2)
end
it "can print all projects" do
Project.all.should eq([#project1, #project2])
end
The failure message I get is:
Project manipulating projects can print all projects
Failure/Error: Project.all.should eq([#project1, #project2])
expected: [Building house, Getting a loan from the Bank]
got: [Building house, Building house, Building house, Getting a loan from the Bank, Building house, Getting a loan from the Bank]
Here is the full spec in a gist: https://gist.github.com/4535863
What am I doing wrong? How can I fix it?
It is doubling the results because it runs the before block for each test, where the class attribute is modified (when two new projects are initialized), and (according to the gist) the test you're referring to is the second one.
To avoid the problem you'll need to reset ##all_projects in an after block:
after do
Project.class_variable_set :##all_projects, []
end
See also: How can I clear class variables between rspec tests in ruby
(Thanks to #iain for the suggestion to move the reset code to an after block rather than a before block.)
This doesn't use before blocks to set stinky instance variables.
describe Project do
let(:options1){
{
name: 'Building house',
priority: 2,
tasks: []
}
}
let(:options2) {
{
name: 'Getting a loan from the Bank',
priority: 3,
tasks: []
}
}
let(:project1) { Project.new(options1) }
let(:project2) { Project.new(options2) }
context "while starting up" do
subject { Project.new options1 }
its(:name) { should include('Building house') }
its(:tasks) { should be_empty }
end
context "manipulating projects" do
before :all do
Project.all.clear
end
subject { Project.all }
its(:count) { should be > 0 }
it { should eq [project1, project2] }
end
end

Resources