The "Tech. Arch."

Architecting Forward ::>

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.
I immediately loved _why’s artistic style, his passion for teaching, as well as the depth of some of the concepts he was trying to present.

On August 12th, 2009, _why went “off-line”,stirring a lot of emotions from the Ruby community.
_why was a prolific developer, artist, and teacher, and inspired many people through his many contributions.

_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:

  1. 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
  2. A core set of base classes to provide some structure to your apps – Camping promotes the Model-View-Controller pattern
  3. Constructs to create your models – Camping leverages the modelling conventions introduced by ActiveRecord
  4. Integration with a persistence engine – Camping uses ActiveRecord
  5. A templating engine for the view – Camping supports layouts
  6. A simplified url routing mechanism for controllers
  7. 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:

  1. There are less moving parts in the framework making it easier to understand
  2. There are fewer conventions to learn about (e.g. compared to Rails)
  3. The minimalistic implementation encourages you to adopt the same approach for your app
  4. You will be able to focus more on the core concepts of your application
  5. 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.

  1. The Index class represents a micro-controller for the / route.
  2. 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.

  1. An index method is defined in the view module.
  2. The view uses Markaby to express the content to be rendered.
  3. 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:

  1. A new module called Models for our ActiveRecord-based models (line 18)
  2. A new model class: Announcement mapped to a micro_announcements table (line 19)
  3. A method to setup the database schema and a demo record (lines 06 and 22)
  4. Retrieval of the announcements in the get method of the Index controller (line 41)
  5. 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:

  1. Log4Net logging
  2. Preload of the Camping gem - with the ability to configure the gem version to use
  3. 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
        
  4. 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:

  1. 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
      
  2. [*'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					
      
  3. 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
      
  4. 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]}"
      
  5. 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*'
      
  6. 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:

  • tweaked the original Camping code
  • tweaked the IronRuby.Rack code
  • added and tweaked the ActiveRecord SQL server adapter
  • created a test database in SQL Server
  • created a login account for the app
  • granted the login account access to the test database
  • configured the ActiveRecord connection
  • added an IIS mapping for the .* extension

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:

  1. Copy C:IronRubybinirake.bat to C:IronRubybinircamping.bat
  2. Copy C:rubybincamping to C:IronRubybinircamping
  3. Create a file named .campingrc in your %USERPROFILE% directory
  4. 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:
  1. _why's code contributions - new mirror on GitHub
  2. judofyr's fork of Camping: active branch of Camping with enhancements
  3. IronRuby v0.9: Source on GitHub
  4. ActiveRecord
  5. ActiveRecord MS SQL Server Adapter
  6. Log4Net: Apache logging for .NET
  7. Markaby Source: Repository on GitHub
  8. Camping Blog Application: Full Camping Example
Sites:
  1. Why's Poignant Guide To Ruby: An cartoon-centric approach to learning Ruby
  2. IronRuby: Main site
  3. IronRuby-Core: Mailing list
  4. Camping - Documentation
  5. Model-View-Controller Pattern
  6. IronRuby Wiki: GitHub wiki
  7. Rack
  8. Deploying Rack-based Ruby Application on IIS: Jimmy Schementi
  9. Markaby Doc: Documentation
  10. Magnus Holm a.k.a. judofyr: Camping project "Coordinator"
  11. John Lam: Blog
  12. RestStop: REST extension for Camping
  13. Camping-PicningSet of extensions
Videos:
  1. Why Camping Matters: Presentation by Nathaniel Talbott
  2. RailsCast Camping Episode 1 of 2
  3. RailsCast Camping Episode 2 of 2
My Other Related Posts:
  1. From Ruby Silverlight to Ruby Web Service (with a touch of spice)

September 11th, 2009 Posted by | Ruby | no comments

No Comments

No comments yet.

Sorry, the comment form is closed at this time.