Saturday, January 28, 2006

 

Puzzling Over Camping

No, I'm not planning anything related to the Great Outdoors. Camping is a microframework, written in Ruby, which provides a model-view-controller web page implementation mechanism similar to Rails. It is, however, much smaller and simpler. I decided I would study it, as a means of learning more about Ruby (and maybe about Rails).

Camping was developed by a programmer known as "Why the Lucky Stiff." He's completely insane, but in a good way. He gave himself a goal of fitting the whole framework into 4K and making it viewable on a single page. To accomplish this, he's eliminated unnecessary whitespace, punctuation, long variable names, and comments. See camping.rb to view the result. (Yes, that really is the actual Camping code, and it really does work.)

Obviously, it's very difficult for a human to read this, especially for a Ruby newbie like myself. So I spent some time reformatting the code and doing a little refactoring for the sake of readability. Here's what I ended up with:

# De-obfuscated rendition of Why the Lucky Stiff's Camping 1.2

require 'rubygems'
require 'active_record'
require 'markaby'
require 'metaid'
require 'ostruct'
require 'tempfile'

module Camping
  C = self

  S = File.read(__FILE__).gsub(/_{2}FILE_{2}/, __FILE__.dump)

  module Helpers
    def R c, *args
      p = /\(.+?\)/
      u = c.urls.detect{|x| x.scan(p).size == args.size}.dup
      args.inject(u){|str, a| str.sub(p,(a.method(a.class.primary_key)[] rescue a).to_s)}
    end

    def / p
      p[/^\//] ? @root+p : p
    end
    
    def errors_for(o)
      ul.errors{o.errors.each_full{|er|li er}} unless o.errors.empty?
    end
  end
  
  module Controllers
    module Base
      include Helpers
      attr_accessor :input, :cookies, :headers, :body, :status, :root

      def method_missing(m, *args, &blk)
        str = m == :render ? markaview(*args, &blk) : eval("markaby.#{m}(*args,&blk)")
        str = markaview(:layout){str} rescue nil
        r(200, str.to_s)
      end

      def r(s, b, h={})
        @status = s
        @headers.merge!(h)
        @body = b
      end

      def redirect(c, *args)
        c = R(c, *args) if c.respond_to? :urls
        r(302, '', 'Location' => self/c)
      end

      def service(r, e, m, a)
        @status, @headers, @root = 200, {}, e['SCRIPT_NAME']
        cook = C.kp(e['HTTP_COOKIE'])
        qs = C.qs_parse(e['QUERY_STRING'])
        if "POST" == m
          inp = r.read(e['CONTENT_LENGTH'].to_i)
          if %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)|n.match(e['CONTENT_TYPE'])
            b = "--#$1"
            inp.split(/(?:\r?\n|\A)#{Regexp::quote(b)}(?:--)?\r\n/m).each{|pt|
              h, v = pt.split("\r\n\r\n", 2)
              fh = {}
              [:name, :filename].each{|x| fh[x]=$1 if h=~/^Content-Disposition: form-data.*(?:\s#{x}="([^"]+)")/m}
              fn = fh[:name]
              if fh[:filename]
                fh[:type] = $1 if h =~ /^Content-Type: (.+?)(\r\n|\Z)/m
                fh[:tempfile] = Tempfile.new("#{C}").instance_eval{binmode; write v; rewind; self}
              else
                fh = v
              end
              qs[fn] = fh if fn
            }
          else
            qs.merge!(C.qs_parse(inp))
          end
        end
        @cookies, @input = [cook, qs].map{|_| OpenStruct.new(_)}
        @body = method(m.downcase).call(*a)
        @headers["Set-Cookie"] = @cookies.marshal_dump.map{|k,v| "#{k}=#{C.escape(v)}; path=/" if v != cook[k]}.compact
        self
      end

      def to_s
        "Status: #{@status}\n#{{'Content-Type'=>'text/html'}.merge(@headers).map{|k,v| v.to_a.map{|v2| "#{k}: #{v2}"}}.flatten.join("\n")}\n\n#{@body}"
      end

      def markaby
        Mab.new(instance_variables.map{|iv| [iv[1..-1],instance_variable_get(iv)]}, {})
      end

      def markaview(m, *args, &blk)
        b = markaby
        b.method(m).call(*args, &blk)
        b.to_s
      end
    end

    class R
      include Base
    end

    class NotFound
      def get(p)
        r(404, div{h1("#{C} Problem!") + h2("#{p} not found")})
      end
    end

    class ServerError
      include Base
      def get(k, m, e)
        r(500, markaby.div{
            h1 "#{C} Problem!"
            h2 "#{k}.#{m}"
            h3 "#{e.class} #{e.message}:"
            ul{e.backtrace.each{|bt| li(bt)}}
          })
      end
    end

    class << self
      def R(*urls)
        Class.new(R){meta_def(:inherited){|c|c.meta_def(:urls){urls}}}
      end

      def D(path)
        constants.inject(nil){|d,c|
          k = const_get(c)
          k.meta_def(:urls){["/#{c.downcase}"]} if !(k < R)
          d || ([k, $~[1..-1]] if k.urls.find { |x| path =~ /^#{x}\/?$/ })} || [NotFound, [path]]
      end
    end
  end

  class << self
    def goes m
      eval(S.gsub(/Camping/, m.to_s), TOPLEVEL_BINDING)
    end
    
    def escape s
      s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n){'%'+$1.unpack('H2'*$1.size).join('%').upcase}.tr(' ','+')
    end

    def unescape(s)
      s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%')].pack('H*')}
    end

    def qs_parse(qs, d ='&;')
      (qs || '').split(/[#{d}] */n).inject({}){|hsh, p|
        k, v = p.split('=', 2).map{|v| unescape(v)}
        hsh[k] = v unless v.blank?
        hsh
      }
    end

    def kp(s)
      c = qs_parse(s, ';,')
    end

    def run(r = $stdin, w = $stdout)
      w << begin
        k, a = Controllers.D "/#{ENV['PATH_INFO']}".gsub(%r!/+!,'/')
        m = ENV['REQUEST_METHOD']||"GET"
        k.class_eval{
          include C
          include Controllers::Base
          include Models
        }
        o = k.new
        o.service(r, ENV, m, a)
      rescue => e
        Controllers::ServerError.new.service(r, ENV, "GET", [k, m, e])
      end
    end
  end

  module Views
    include Controllers
    include Helpers
  end

  module Models
  end

  Models::Base = ActiveRecord::Base

  class Mab < Markaby::Builder
    include Views

    def tag!(*g, &b)
      h = g[-1]
      [:href,:action].each{|a| (h[a] = self/h[a]) rescue 0}
      super
    end
  end
end

Now it doesn't look so scary. If I'm lucky, it will actually work the same way as the original.

This doesn't exactly match the current version of Why's camping.rb, because he's been making changes. Doing the reformatting exercise manually was valuable experience, but I started looking for an automated way of doing it.

Then I discovered, yet again, that I am a bonehead. Why already provides a nice readable version of the Camping code: camping-unabridged.rb. It's even got comments and inline documentation.

But that takes all the fun out of it. I'm going to keep looking at "my" version to figure it out. These kids today have it too easy.


Comments:
I'm glad I'm not a programmer anymore... cobol on the mainframe was so much easier to read.

Happy belated birthday Kris.
 
This comment has been removed by a blog administrator.
 
Post a Comment

<< Home

This page is powered by Blogger. Isn't yours?