I'm currently using Mongrel to develop a custom web application project.
I would like Mongrel to use a defined Http Handler based on a regular expression. For example, everytime someone calls a url like http://test/bla1.js or http://test/bla2.js the same Http handler is called to manage the request.
My code so far looks a like that:
http_server = Mongrel::Configurator.new :host => config.get("http_host") do
listener :port => config.get("http_port") do
uri Regexp.escape("/[a-z0-9]+.js"), :handler => BLAH::CustomHandler.new
uri '/ui/public', :handler => Mongrel::DirHandler.new("#{$d}/public/")
uri '/favicon', :handler => Mongrel::Error404Handler.new('')
trap("INT") { stop }
run
end
end
As you can see, I am trying to use a regex instead of a string here:
uri Regexp.escape("/[a-z0-9]+.js"), :handler => BLAH::CustomHandler.new
but that does not work. Any solution?
Thanks for that.
You should consider creating a Rack application instead. Rack is:
the standard for Ruby web applications
used internally by all popular Ruby web frameworks (Rails, Merb, Sinatra, Camping, Ramaze, ...)
much easier to extend
ready to be run on any application server (Mongrel, Webrick, Thin, Passenger, ...)
Rack has a URL mapping DSL, Rack::Builder, which allows you to map different Rack applications to particular URL prefixes. You typically save it as config.ru, and run it with rackup.
Unfortunately, it does not allow regular expressions either. But because of the simplicity of Rack, it is really easy to write an "application" (a lambda, actually) that will call the proper app if the URL matches a certain regex.
Based on your example, your config.ru may look something like this:
require "my_custom_rack_app" # Whatever provides your MyCustomRackApp.
js_handler = MyCustomRackApp.new
default_handlers = Rack::Builder.new do
map "/public" do
run Rack::Directory.new("my_dir/public")
end
# Uncomment this to replace Rack::Builder's 404 handler with your own:
# map "/" do
# run lambda { |env|
# [404, {"Content-Type" => "text/plain"}, ["My 404 response"]]
# }
# end
end
run lambda { |env|
if env["PATH_INFO"] =~ %r{/[a-z0-9]+\.js}
js_handler.call(env)
else
default_handlers.call(env)
end
}
Next, run your Rack app on the command line:
% rackup
If you have mongrel installed, it will be started on port 9292. Done!
You have to inject new code into part of Mongrel's URIClassifier, which is otherwise blissfully unaware of regular expression URIs.
Below is one way of doing just that:
#
# Must do the following BEFORE Mongrel::Configurator.new
# Augment some of the key methods in Mongrel::URIClassifier
# See lib/ruby/gems/XXX/gems/mongrel-1.1.5/lib/mongrel/uri_classifier.rb
#
Mongrel::URIClassifier.class_eval <<-EOS, __FILE__, __LINE__
# Save original methods
alias_method :register_without_regexp, :register
alias_method :unregister_without_regexp, :unregister
alias_method :resolve_without_regexp, :resolve
def register(uri, handler)
if uri.is_a?(Regexp)
unless (#regexp_handlers ||= []).any? { |(re,h)| re==uri ? h.concat(handler) : false }
#regexp_handlers << [ uri, handler ]
end
else
# Original behaviour
register_without_regexp(uri, handler)
end
end
def unregister(uri)
if uri.is_a?(Regexp)
raise Mongrel::URIClassifier::RegistrationError, "\#{uri.inspect} was not registered" unless (#regexp_handlers ||= []).reject! { |(re,h)| re==uri }
else
# Original behaviour
unregister_without_regexp(uri)
end
end
def resolve(request_uri)
# Try original behaviour FIRST
result = resolve_without_regexp(request_uri)
# If a match is not found with non-regexp URIs, try regexp
if result[0].blank?
(#regexp_handlers ||= []).any? { |(re,h)| (m = re.match(request_uri)) ? (result = [ m.pre_match + m.to_s, (m.to_s == Mongrel::Const::SLASH ? request_uri : m.post_match), h ]) : false }
end
result
end
EOS
http_server = Mongrel::Configurator.new :host => config.get("http_host") do
listener :port => config.get("http_port") do
# Can pass a regular expression as URI
# (URI must be of type Regexp, no escaping please!)
# Regular expression can match any part of an URL, start with "^/..." to
# anchor match at URI beginning.
# The way this is implemented, regexp matches are only evaluated AFTER
# all non-regexp matches have failed (mostly for performance reasons.)
# Also, for regexp URIs, the :in_front is ignored; adding multiple handlers
# to the same URI regexp behaves as if :in_front => false
uri /^[a-z0-9]+.js/, :handler => BLAH::CustomHandler.new
uri '/ui/public', :handler => Mongrel::DirHandler.new("#{$d}/public/")
uri '/favicon', :handler => Mongrel::Error404Handler.new('')
trap("INT") { stop }
run
end
end
Seems to work just fine with Mongrel 1.1.5.
Related
I am creating my Api in Sinatra but i want for example use this routes:
/places
/places.meta
/places.list
/users/id/places.list
i have this working in rails but fails in sinatra
def index
case request.format.to_sym.to_s
when 'list'
result = Place.single_list(parameters)
when 'meta'
result = #parameters.to_meta
else
result = Place.get_all(parameters)
end
render json: result, status: 200
end
Sinatra doesn't have a built-in concept of a "request format", so you have to manually specify a format-aware route pattern which Rails provide you automatically.
Here I use route pattern specified as a Regexp with a named capture:
require 'sinatra'
get /\/places(\.(?<format>meta|list))?/ do # named capture 'format'
case params['format'] # params populated with named captures from the route pattern
when 'list'
result = Place.single_list(parameters)
when 'meta'
result = #parameters.to_meta
else
result = Place.get_all(parameters)
end
result.to_json # replace with your favourite way of building a Sinatra response
end
I am trying to move stuff at root to /en/ directory to make my little service multi-lingual.
So, I want to redirect this url
mysite.com/?year=2018
to
mysite.com/en/?year=2018
My code is like
get %r{^/(\?year\=\d{4})$} do |c|
redirect "/en/#{c}"
end
but it seems like I never get #{c} part from the url.
Why is that? or are there just better ways to do this?
Thanks!
You can use the request.path variable to get the information you're looking for.
For example,
get "/something" do
puts request.path # => "/something"
redirect "/en#{request.path}"
end
However if you are using query parameters (i.e. ?yeah=2000) you'll have to manually pass those off to the redirect route.
Kind of non-intuitively, there's a helper method for this in ActiveRecord.
require 'active_record'
get "/something" do
puts params.to_param
# if params[:year] is 2000, you'll get "year=2000"
redirect "/en#{request.path}?#{params.to_param}"
end
You could alternatively write your own helper method pretty easily:
def hash_to_param_string(hash)
hash.reduce("") do |string, (key, val)|
string << "#{key}=#{val}&"
end
end
puts hash_to_param_string({key1: "val1", key2: "val2"})
# => "key1=val1&key2=val2"
There seem to be several options for establishing Redis connections for use within EventMachine, and I'm having a hard time understanding the core differences between them.
My goal is to implement Redis within Goliath
The way I establish my connection now is through em-synchrony:
require 'em-synchrony'
require 'em-synchrony/em-redis'
config['redis'] = EventMachine::Synchrony::ConnectionPool.new(:size => 20) do
EventMachine::Protocols::Redis.connect(:host => 'localhost', :port => 6379)
end
What is the difference between the above, and using something like em-hiredis?
If I'm using Redis for sets and basic key:value storage, is em-redis the best solution for my scenario?
We use em-hiredis very successfully inside Goliath. Here's a sample of how we coded publishing:
config/example_api.rb
# These give us direct access to the redis connection from within the API
config['redisUri'] = 'redis://localhost:6379/0'
config['redisPub'] ||= EM::Hiredis.connect('')
example_api.rb
class ExampleApi < Goliath::API
use Goliath::Rack::Params # parse & merge query and body parameters
use Goliath::Rack::Formatters::JSON # JSON output formatter
use Goliath::Rack::Render # auto-negotiate response format
def response(env)
env.logger.debug "\n\n\nENV: #{env['PATH_INFO']}"
env.logger.debug "REQUEST: Received"
env.logger.debug "POST Action received: #{env.params} "
#processing of requests from browser goes here
resp =
case env.params["action"]
when 'SOME_ACTION' then process_action(env)
when 'ANOTHER_ACTION' then process_another_action(env)
else
# skip
end
env.logger.debug "REQUEST: About to respond with: #{resp}"
[200, {'Content-Type' => 'application/json', 'Access-Control-Allow-Origin' => "*"}, resp]
end
# process an action
def process_action(env)
# extract message data
data = Hash.new
data["user_id"], data["object_id"] = env.params['user_id'], env.params['object_id']
publishData = { "action" => 'SOME_ACTION_RECEIVED',
"data" => data }
redisPub.publish("Channel_1", Yajl::Encoder.encode(publishData))
end
end
return data
end
# process anothr action
def process_another_action(env)
# extract message data
data = Hash.new
data["user_id"], data["widget_id"] = env.params['user_id'], env.params['widget_id']
publishData = { "action" => 'SOME_OTHER_ACTION_RECEIVED',
"data" => data }
redisPub.publish("Channel_1", Yajl::Encoder.encode(publishData))
end
end
return data
end
end
Handling subscriptions are left as an exercise for the reader.
what em-synchrony does is patch the em-redis gem to allow using it with fibers which effectively allows it to run in goliath.
Here is a project using Goliath + Redis which can guide you on how to make all this works: https://github.com/igrigorik/mneme
Example with em-hiredis, what goliath do is wrap your request in a fiber so a way to test it is:
require 'rubygems'
require 'bundler/setup'
require 'em-hiredis'
require 'em-synchrony'
EM::run do
Fiber.new do
## this is what you can use in goliath
redis = EM::Hiredis.connect
p EM::Synchrony.sync redis.keys('*')
## end of goliath block
end.resume
end
and the Gemfile I used:
source :rubygems
gem 'em-hiredis'
gem 'em-synchrony'
If you run this example you will get the list of defined keys in your redis database printed on screen.
Without the EM::Synchrony.sync call you would get a deferrable but here the fiber is suspended until the calls return and you get the result.
Current code works as long as there is no remote error:
def get_name_from_remote_url
cstr = "http://someurl.com"
getresult = open(cstr, "UserAgent" => "Ruby-OpenURI").read
doc = Nokogiri::XML(getresult)
my_data = doc.xpath("/session/name").text
# => 'Fred' or 'Sam' etc
return my_data
end
But, what if the remote URL times out or returns nothing? How I detect that and return nil, for example?
And, does Open-URI give a way to define how long to wait before giving up? This method is called while a user is waiting for a response, so how do we set a max timeoput time before we give up and tell the user "sorry the remote server we tried to access is not available right now"?
Open-URI is convenient, but that ease of use means they're removing the access to a lot of the configuration details the other HTTP clients like Net::HTTP allow.
It depends on what version of Ruby you're using. For 1.8.7 you can use the Timeout module. From the docs:
require 'timeout'
begin
status = Timeout::timeout(5) {
getresult = open(cstr, "UserAgent" => "Ruby-OpenURI").read
}
rescue Timeout::Error => e
puts e.to_s
end
Then check the length of getresult to see if you got any content:
if (getresult.empty?)
puts "got nothing from url"
end
If you are using Ruby 1.9.2 you can add a :read_timeout => 10 option to the open() method.
Also, your code could be tightened up and made a bit more flexible. This will let you pass in a URL or default to the currently used URL. Also read Nokogiri's NodeSet docs to understand the difference between xpath, /, css and at, %, at_css, at_xpath:
def get_name_from_remote_url(cstr = 'http://someurl.com')
doc = Nokogiri::XML(open(cstr, 'UserAgent' => 'Ruby-OpenURI'))
# xpath returns a nodeset which has to be iterated over
# my_data = doc.xpath('/session/name').text # => 'Fred' or 'Sam' etc
# at returns a single node
doc.at('/session/name').text
end
I am trying to write two kind of Rack routes. Rack allow us to write such routes like so:
app = Rack::URLMap.new('/test' => SimpleAdapter.new,
'/files' => Rack::File.new('.'))
In my case, I would like to handle those routes:
"/" or "index"
"/*" in order to match any other routes
So I had trying this:
app = Rack::URLMap.new('/index' => SimpleAdapter.new,
'/' => Rack::File.new('./public'))
This works well, but... I don't know how to add '/' path (as alternative of '/index' path). The path '/*' is not interpreted as a wildcard, according to my tests. Do you know how I could do?
Thanks
You are correct that Rack::URLMap doesn't treat '*' in a path as a wildcard. The actual translation from path to regular expression looks like this:
Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
That is, it treats any characters in the path as literals, but also matches a path with any suffix. I believe the only way for you to accomplish what you're attempting is to use a middleware instead of an endpoint. In your config.ru you might have something like this:
use SimpleAdapter
run Rack::File
And your lib/simple_adapter.rb might look something like this:
class SimpleAdapter
SLASH_OR_INDEX = %r{/(?:index)?}
def initialize(app)
#app = app
end
def call(env)
request = Rack::Request.new(env)
if request.path =~ SLASH_OR_INDEX
# return some Rack response triple...
else
# pass the request on down the stack:
#app.call(env)
end
end
end