This is admittedly a rare need, but the other day I found myself needing to
conditionally enable and disable sessions in Sinatra on a per-request basis.
Sinatra uses the Rack::Session middleware to manage sessions. I searched
around for a while but couldn’t find out how to do it. There are options for
the session that can be set per-request, like this:
123
get'/'dorequest.session_options[:renew]=trueend
The documentation shows a :defer option that seems to do what I want, but the terminology didn’t make it abundantly clear whether it did or not.
:defer will not set a cookie in the response.
Actually that does sound like what I want, sorta, but mentally I was
thinking “turn off” or “disabling” and not “deferring the setting of a cookie.”
With most ruby projects, or for that matter, any interpreted language that isn’t compiled
, I’ve found that I end up spending a decent amount of time reading the
code from my dependencies. Some unscientific polling of ruby devs I know showed
that to be a pretty standard practice.
Doing a little digging through the Rack::Session middleware showed an
option that didn’t appear in the documentation:
:skip will not a set a cookie in the response nor update the session state
Now that matches the mental model of what I was looking for, and the
behavior as well. Turns out it’s not in the docs because it was added
recently from a Rails core team
member to support the new asset pipeline in Rails 3.1, and the docs
haven’t been updated yet.
So if, on a per-request basis, you want to disable using sessions
entirely, simply do:
(where “use_session?” is your method that figures out whether or not to
use sessions)
What’s the diff between :defer and :skip, you ask? Well the answer is in
the pull request description
This will not send a cookie back nor change the session state.
The :defer option did not send the cookie back but did change the session
state in the backend.
This is useful for assets requests that still go through the rack stack
but do not want to cause any change in the session (for example accidentally expiring flash messages).
My last post about introducing an extensibility point
got some good feedback. I realized two things:
Following the pattern of rails generators was huge overkill
I wasn’t thinking correctly about the problem to begin with
Rails generators need to support a lot of things. For example, they
need to process command line arguments, consume or produce templates, and update
existing files in a non-destructive way. The potential complexity in
generators is handled by the complexity in the extensility model
implemented in rails. Arbitrarily duplicating this complexity (magic class names,
inheritance, etc) is just wrong. Which, of course, is why I
said:
OK, I’m doing something wrong, I just know it
If I was going to continue down this road (I’m not, see below) I would
take a cue from something like Sinatra and take advantage
of global methods to reduce/remove requirements on the configuration
code.
123
configuredo|host|# do something with 'host' to configure your append
But that’s all irrelevent. Jeff Lindsay set me straight in a comment on my
previous post.
The hooks that most SCM use, like post-commit or post-receive,
are based on running shell scripts. This is my favorite approach
so far because it’s not language specific.
DUH. I love those things that I look back and think “of course.”
I was thinking about this as a ruby problem, because localtunnel’s
client is written in ruby. But it’s not a ruby problem. Localtunnel is a
utility. I can write hooks for Git in anything I want, it doesn’t matter
what language Git is written in. The same is true for localtunnel.
Taking this approach greatly simplifies the solution. And the simpler
solution is always always always the better solution. In this
case, we can add a quick check followed by a system call to enable users
to provide a shell hook to configure whatever system(s) they want:
1234567891011
SHELL_HOOK_FILE="./.localtunnel_callback"...ifFile.exists?(File.expand_path(SHELL_HOOK_FILE))system"#{SHELL_HOOK_FILE} ""#{tunnel['host']}"""ifFile.exists?(File.expand_path(SHELL_HOOK_FILE))if!$?.success?puts" An error occurred executing the callback hook #{SHELL_HOOK_FILE}"puts" (Make sure it is executable)"endend
Bam. Localtunnel now provides an extensibility point that can be
implemented in any language. Well, it will when/if my pull request is
accepted. :)
I ran into an interesting problem recently. There is a utility
called localtunnel which connects a public URL to a local port. Extremely
useful when it comes to developing an app that leverages services that
expose web hooks.
The problem is that the public URL is only valid while the script is running. Which means every time you start it, you potentially need to go update the web hook URL in whatever service you’re using. For example, with Twilio you configure an app that is available at a particular URL. With GitHub you can add post-receive hooks.
What needs to happen is that localtunnel needs to give you a chance to run some
custom code after it registers and connects up ports, but before it sits
and waits.
It needs an extension point.
One fork seeks to do this by having localtunnel call a web hook. It works, even if it feels a bit ironic. ;)
But that involves maintaining another service simply to configure your
first service, which is less than ideal. I took a different approach and I’d love to get feedback here, since I’m
still so new to this.
Most stuff on the web about extending ruby has to do with class design,
or monkeypatching, or something else that assumes all the code is
already loaded and executing. In this case, there’s an existing app (not framework)
and I want to have it load my code dynamically at runtime. In my code,
I want to configure whatever service I need to.
Being a n00b, I’m not the most widely read yet, so I looked at an
example of something that I already knew exposed an extension point in a
similar way: rails generators.
This part was easy, I just extended the existing command line processing
to add a “-c NAME” argument specifying which configuration you want to run.
For example:
1
$ localtunnel -c twilio 9292
will create a public URL connecting to local port 9292, and will look
for an auto configuration implementation named “twilio” and execute it.
Discovery via magic names, I mean, convention
This is a nice, but sometimes frustrating thing about Rails. There’s
lots of stuff that magically happens if you organize or name your code a
certain way.
In this case, to make it easy to specify via the command line, I opted
for a similar “magic name” approach. Name your file
foo_auto_config.rb and when you specify “-c foo” I’ll look for your
file under a ‘localtunnel’ subdir. This is a way to try and avoid loading a lot of code unnecessarily.
defself.lookup(name)including_current=$LOAD_PATH.dupincluding_current<<'.'including_current.eachdo|base|Dir[File.join(base,"localtunnel","#{name}_auto_config.rb")].eachdo|path|beginrequirepathrescueException=>eputs" [Warning] Could not load autoconfig #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"endendendend
This has a few issues with it. Because this is running as a script, if
you’re using bundler, the $LOAD_PATH won’t include all the gems unless
Bundler.require has been called. So looking in the load path for auto
configuration files is probably pointless. Not that gems would include a
custom auto configuration anyway. I should probably remove this.
(Thoughts?) Secondly, I manually added
the local directory because ruby 1.9.2 took it out of $LOAD_PATH by default due to security reasons.
And if you consider the use-case, this is typically executed from within
~/MyApp and not randomly throughout the directory structure. Again,
similar to rails generators, you run it from the root of your
application directory.
Discovery via class name + base class
Now that we’ve figured out which files to load, we still don’t know what
code to call. We need to be able to pass in parameters such as the new
URL that is reserved, so we probably want a method to call and not just loading a file.
Ruby gives us a feature that allows you to know when a new base class is
created: the inherited method.
It gets called when a new subclass is created.
I created a base class, LocalTunnel::AutoConfig::Base, which keeps track
of all subclasses:
When someone specifies “-c foo” on the command line, I look for any
subclass of LocalTunnel::AutoConfig::Base that is named FooAutoConfig
and has a method called “configure”. If I find that, then that’s the
auto configuration code that will be called.
if!@autoconfig.nil?configurator=LocalTunnel::AutoConfig.find(@autoconfig)ifconfiguratorconfigurator.configure(tunnel['host'])elseputs" [Warning] Unable to find an automatic configuration plugin for '#{@autoconfig}'"endend
Do your custom configuration
The configuration code can then run and do whatever it wants. Usually it’s
specific per app. Here’s an example of me using this to configure
Twilio.
twilio_auto_config.rb
1234567891011121314151617181920212223242526
require'rubygems'require'localtunnel/autoconfig'require'twilio-ruby'require'uri'classTwilioAutoConfig<LocalTunnel::AutoConfig::BaseTWILIO_ACCOUNT_SID=# my account SidTWILIO_AUTH_TOKEN=ENV['TWILIO_AUTH_TOKEN']TWILIO_APP_SID=ENV['TWILIO_APP_SID']defconfigure(host)# set up a client to talk to the Twilio REST APIclient=Twilio::REST::Client.new(TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN)app=client.account.applications.get(TWILIO_APP_SID)# Grab the current voice_url and status_callback and swap out the hostvoice=URI.parse(app.voice_url)voice.host=hoststatus_callback=URI.parse(app.status_callback)status_callback.host=hostapp.update({:voice_url=>voice.to_s,:status_callback=>status_callback.to_s})puts" Configured twilio app #{app.friendly_name} for new host #{host}"endend
OK, I’m doing something wrong, I just know it
I am sure I either overcomplicated this, or just plain did it wrong.
Some thoughts I have (in hindsight now) are that perhaps I could’ve
exposed a global method for the autoconfig code to call. Then, instead
of a base class and magic class name, I’d just load the specified file
(magic name is still nice) and in that file I could call “host” to get
the new host name.
Leave a comment, create a gist, fork my fork,
or tweet at me, but somehow let me know how I could
make this better.
It’s been a long time since I’ve spent significant time to learn an entirely new technology. And I’ve had a blast digging into ruby, rails, sinatra, padrino, javascript (okay, I really mean jQuery), and tons of other related frameworks. In spite of some of the excellent, and not-so-excellent, documentation out there, it can be an incredibly frustrating experience. Especially when trying to learn something new – like something really new – not just new to me. Expect to see posts here related to what I’m learning, that make as few assumptions as possible about what you already know.
For example, I recently worked on getting a test environment set up for my app that involves sinatra, mongoid, and twilio APIs. It was an interesting experience. I’m sure seasoned ruby devs wouldn’t have had the same issues, but I’m a firm believer in “you’re not alone” so I’ll be posting about the process to hopefully help someone else in my shoes.
For the past 6 months to a year, I’ve been getting more and more into
web development on the side (I used to be a web developer, a long time
ago.) I’ve been focusing exclusively on ruby-based frameworks. It serves
two purposes, I get to learn a new language, and there are plenty of
options out there for me to explore.
In that spirit, instead of changing the focus of my existing
blog, I’m
going to take a cue from Ben
Scheirman and
start a new blog and just start updating this one more often. Which,
given the number of things going on in my life right now, will not be
often at all. :)
If you haven’t checked out octopress you should
- it’s pretty sweet! I love the idea of a file-based blog engine using
git.