Running the Camping Microframework on IronRuby
![]() |
Homage to _why
Six or seven years ago, when I read about Ruby, I quickly came across Why’s Poignant Guide To Ruby. On August 12th, 2009, _why went “off-line”,stirring a lot of emotions from the Ruby community. _why’s code treasure trove included Camping, a micro-framework aimed at providing a lean base for your web apps. |
![]() |
Camping, a Micro-Framework
A micro-framework consists of:
- The bare essentials to host a web app – Camping leverages Rack for the base request and response processing but provides its own basic session mechanim
- A core set of base classes to provide some structure to your apps – Camping promotes the Model-View-Controller pattern
- Constructs to create your models – Camping leverages the modelling conventions introduced by ActiveRecord
- Integration with a persistence engine – Camping uses ActiveRecord
- A templating engine for the view – Camping supports layouts
- A simplified url routing mechanism for controllers
- An easy way to access session and form data inside controllers
Why a micro-framework as opposed to a regular framework? Well consider the following benefits:
- There are less moving parts in the framework making it easier to understand
- There are fewer conventions to learn about (e.g. compared to Rails)
- The minimalistic implementation encourages you to adopt the same approach for your app
- You will be able to focus more on the core concepts of your application
- The resulting total footprint is much smaller and tighter making it possible to run under more stringent infrastructure constraints
A micro-framework like Camping provides just enough structure and core functionality to let you incrementally design and build your application.
Once your application reaches a higher level of sophistication and technical feature requirements, you can migrate the core app to a more feature-rich framework like Rails for example.
Also there is less “friction” when you want to prototype an application to figure out the core concepts. I have been using Camping to try out some ideas,
then realized that the implementation was just fine, so I just went ahead and deployed it as-is. The lower friction comes into play if you are building some of these apps in your own time
where you want to maximize your productive time investment and make measurable progress in less time.
Nathaniel Talbott expanded on that point in his “Why Camping Matters” presentation.
To get started, download IronRuby and get Camping:
gem install camping
Camping Examples
Below are 3 progressively more functional examples illustrating the Model-View-Controller approach in Camping.
A) Controller Sample
Here is a very small example similar to a Pico-Sinatra application with a module including just a controller with one single route.
- The Index class represents a micro-controller for the / route.
- A get method / action is defined for the HTTP GET method on that route.
require 'camping' Camping.goes :Pico module Pico def Pico.create end end module Pico::Controllers class Index < R '/' def get "Welcome to the Pico Camping app - #{Time.now}" end end end
B) Controller And View Sample
Here is a variation of the previous example with a corresponding view module providing a layout serving as a template very much like in Rails.
- An index method is defined in the view module.
- The view uses Markaby to express the content to be rendered.
- The get method on the Index controller calls the render :index method to select the view to render.
require 'camping' Camping.goes :Nano module Nano def Nano.create end end module Nano::Controllers class Index < R '/' def get render :index end end end module Nano::Views def layout html do body do self << yield hr div.footer! { "#{Time.now}" } end end end def index h3 "Welcome to the Nano Camping app!" end end
C) Model View Controller Sample
Here is a variation of the previous example with the following additions:
- A new module called Models for our ActiveRecord-based models (line 18)
- A new model class: Announcement mapped to a micro_announcements table (line 19)
- A method to setup the database schema and a demo record (lines 06 and 22)
- Retrieval of the announcements in the get method of the Index controller (line 41)
- Rendering of the announcements in the index method of the view (line 60)
require 'camping' Camping.goes :Micro module Micro def Micro.create Camping::Models::Base.establish_connection :adapter => 'mssql', :host => 'localhost', :database => 'camping', :username => '*USERNAME*', :password => '*PASSWORD*' Micro::Models.create_schema :assume => (Micro::Models::Announcement.table_exists? ? 1.0 : 0.0) end end module Micro::Models class Announcement < Base end class SetupTheDB < V 1.0 def self.up create_table :micro_announcements, :force => true do |t| t.column :id, :integer, :null => false t.column :title, :string, :limit => 255 end Announcement.create :title => 'Welcome to the Micro Camping app!' end def self.down drop_table :micro_announcements end end end module Micro::Controllers class Index < R '/' def get @announcements = Announcement.find(:all) render :index end end end module Micro::Views def layout html do body do self << yield hr div.footer! { "#{Time.now}" } end end end def index @announcements { | a | h3 a } end end
These three examples are minimalistic. You have access to the full power of ActiveRecord to express relationships between models, add validation and constraints.
At the controller level Camping also provide a really easy way to access both form data, querystring parameters, and session parameters through hashes (HashWithIndifferentAccess).
- The input variable, available at the controller level, holds on to all form data and querystring parameters.
- The @state variable, available at the view and controller levels, holds all session data
So don't let these examples fool you, the micro-framework is not as "micro" as you might think! For a richer example, check-out the Camping Blog example.
To test Camping on IronRuby, I started with the basic example (pico) and then progressively worked up the level of functionality.
IronRuby Camp Site
I have been actively following the development of IronRuby since the days Microsoft hired John Lam to work on IronRuby.
(See my other post(s) on the topic).
I got pretty excited when I ready Jimmy Schementi's post on getting Rack to run on IIS.
This opened the ability to run Camping since it has very lightweight needs and works on top of Rack.
So as a prerequisite I started to install IronRack and its example.
Unfortunately I could not get the example to work - maybe due to some of the permissions around the LogUtils component.
So I built a slightly altered version / configuration and switched logging to Log4Net.
And bingo! I was able to run the basic IronRack example.
I then created a custom IronCamping web app, modeled very closely after the IronRack model with a few additions:
- Log4Net logging
- Preload of the Camping gem - with the ability to configure the gem version to use
- Customized the config.ru (Rack Up) to start a very tiny Camping app:
require 'rubygems' require 'rack' require 'camping' require 'demo' # where my Camping demo code resides # Initialize the Demo module using the create method Demo.create # Instantiate a Rack-Camping adapter for our Demo module demo_module_adapter = Rack::Adapter::Camping.new(Demo) # Run the Camping Demo app run demo_module_adapter
- I also configured my IIS virtual directory to explicitly map the .* to the ASP.NET ISAPI module
Need A Few More Poles For The Tent!
Ok, so I could not get the famous raising of the tent to work using the traditional statement:
Camping.goes :Pico
So I started to really read the (unabridged!) Camping framework code
to understand the core as well as to isolate code fragments
and compare their execution in IronRuby and in standard Ruby (MRI).
Once I got the minimalistic app to come up, I moved on to _why's Camping Blog application.
I ran into additional sets of issues which required more digging, analyzing and testing.
Here is a summary of my findings and work-arounds:
- The "goes" statement performs some text substitutions in the basic camping module, loads and binds them to the TOPLEVEL_BINDING.
Apparently at the time of this writing, that global constant was not set by the base IronRack code.
A suggestion was made on the mailing list to update the RubyEngine class of IronRack.
I will update the post once this is available.The work-around was to explicitly require the 'hacks.rb' module from IronRuby like so:
require 'hacks' # for TOPLEVEL_BINDING require 'camping' Camping.goes :Pico
- [*'a'..'b'] does not work in IR the same way it does in MRI.
As a result, on line 27 of camping/session.rb the session id cannot be generated correctly leading to an error.The solution was to explicitly do a to_a conversion and rewrite line 27 as:
# OLD: RAND_CHARS = [*'A'..'Z'] + [*'0'..'9'] + [*'a'..'z'] # NEW: User standard interval notation plus an explicit to_a to return an array RAND_CHARS = ('A'..'Z').to_a + ('0'..'9').to_a + ('a'..'z').to_a
- Not sure exactly why but when the session is loaded, the @state is not restored as a HashWithIndifferentAccess
The solution was to explicitly cast the state as a HashWithIndifferentAccess to explicitly re-cast the hash as a HashWithIndifferentAccess after line 111 of the
service(*a) method of the Session module in session.rb, right after the line initializing @state:@state = (session[app] ||= Camping::H[]) @state = Camping::H[@state] # Explicit re-cast as a HashWithIndifferentAccess
- On line 399 (of the camping-unabridged.rb),
the code creating the value to be written to the document cookie returns an array instead of a string.
This causes the cookie to not be populated correctly which prevents the session from working.The solution was to rewrite the line:
# OLD: @headers['Set-Cookie'] = @cookies.map { |k,v| "#{k}=#{C.escape(v)}; path=#{self/"/"}" if v != @k[k] } - [nil] # NEW: evaluates and renders the expression as a string using "#{}" @headers["Set-Cookie"] = "#{@cookies.map{|k,v|"#{k}=#{C.escape(v)}; path=#{self/'/'}" if v != @k[k] } - [nil]}"
- Since I would like the app to run on MS SQL Server, I added the ActiveRecord MS SQL adapter
to my activerecord-2.3.3libactive_recordconnection_adapters folder.
I also made a change to the includes_id_field
method to ensure that the SET IDENTITY_INSERT statement is only generated for the id column.def includes_id_field(sql) sql =~ /[id]|[w*_id]/i end
Then I added the following ActiveRecord configuration statement to my application module:
Camping::Models::Base.establish_connection :adapter => 'mssql', :host => 'localhost', :database => 'camping', :username => '*USERNAME*', :password => '*PASSWORD*'
- Finally, the harder problem I had to track down was that the setup of the rack.input context for Rack
(line 159) in the IIS class for IronRack.env["rack.input"] = RubyEngine.Execute("StringIO.new(__request.body || '')", handle_scope);
The setup would work for a GET but not a POST with form data. This would trigger a [InvalidOperationException: can't convert System::String into String] in the DLR.
The solution was to rewrite the line as follows:if (request.Body != null) { // Explicitly pass the request body as a string string myInput = request.Body.ToString(); handle_scope.SetVariable("__myinput", myInput); env["rack.input"] = RubyEngine.Execute("StringIO.new(__myinput.to_s)", handle_scope); } else { // Original code env["rack.input"] = RubyEngine.Execute("StringIO.new(__request.body || '')", handle_scope); }
Raising The Camp Flag
So in summary, once I had:
I was then able to get the Camping Blog app to run! Yippee! |
![]() |
As a footnote, I noticed that the new branch of the Camping code maintained by Judofyr includes more recent changes
that may not require the Camping tweaks I made. I will update this post once I get a chance to test that thoroughly.
Running In The IronRuby Console
If you want the ability to run Camping from the command line IronRuby console, you also need to:
- Copy C:IronRubybinirake.bat to C:IronRubybinircamping.bat
- Copy C:rubybincamping to C:IronRubybinircamping
- Create a file named .campingrc in your %USERPROFILE% directory
- Edit .campingrc and add the following content:
database: adapter: mssql host: localhost database: camping username: *USERNAME* password: *PASSWORD*
Then you can start your Camping app as follows:
ircamping micro.rb
You can then open up a browser and go to http://localhost:3301/
So What?
I really like the power of Rails for building elaborate apps. But when I want to prototype a concept really quickly,
I enjoy using Camping because of its minimalistic approach combined with the structure of MVC.
And having most of the code in a single file (albeit with some structure) makes it very convenient to make quick changes and test immediately
(thanks to the Camping reloader)
Another nice thing is that Camping has such a small footprint that you can use it on shared web hosts with little processing overhead.
This makes it thin enough to serve REST services using the Restop framework.
Thanks to all the great work of the IronRuby team, I can now enjoy Camping on .NET.
I might even start mixing in .NET classes!!!
Hopefully this will give you an idea of what is possible with Camping and IronRuby.
References and Resources
Software:
- _why's code contributions - new mirror on GitHub
- judofyr's fork of Camping: active branch of Camping with enhancements
- IronRuby v0.9: Source on GitHub
- ActiveRecord
- ActiveRecord MS SQL Server Adapter
- Log4Net: Apache logging for .NET
- Markaby Source: Repository on GitHub
- Camping Blog Application: Full Camping Example
Sites:
- Why's Poignant Guide To Ruby: An cartoon-centric approach to learning Ruby
- IronRuby: Main site
- IronRuby-Core: Mailing list
- Camping - Documentation
- Model-View-Controller Pattern
- IronRuby Wiki: GitHub wiki
- Rack
- Deploying Rack-based Ruby Application on IIS: Jimmy Schementi
- Markaby Doc: Documentation
- Magnus Holm a.k.a. judofyr: Camping project "Coordinator"
- John Lam: Blog
- RestStop: REST extension for Camping
- Camping-PicningSet of extensions
Videos:
- Why Camping Matters: Presentation by Nathaniel Talbott
- RailsCast Camping Episode 1 of 2
- RailsCast Camping Episode 2 of 2