Karl Oscar Weber

Ruby Camping

Hi Friends,

Camping is a micro web-framework written in Ruby. Originally written by _why the lucky stiff, (Their real name), Camping was a pretty popular framework a few years ago, but over time just slowed down. Now Camping is actively maintained and improving.

One of the best things about Camping is how compact it is, The whole thing is about 4kb:

require"uri";require"rack";E||="Content-Type";Z||="text/html"
class Object;def meta_def m,&b;(class<<self;self
end).send:define_method,m,&b end;end;module Camping;C=self;S=IO.read(__FILE__
)rescue nil;P="<h1>Cam\ping Problem!</h1><h2>%s</h2>";U=Rack::Utils;O={url_prefix:""};Apps=[];
SK=:camping;G=[];class H<Hash;def method_missing m,*a;m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m.
to_s]:super end;undef id,type if ??==63 end;class Cookies<H;attr_accessor :_p;
def _n;@n||={}end;alias _s []=;def set k,v,o={};_s(j=k.to_s,v);_n[j]=
{:value=>v,:path=>_p}.update o;end;def []=(k,v)set(k,v,v.is_a?(Hash)?v:{})end
end;module Helpers;def R c,*g;p,h=
/\(.+?\)/,g.grep(Hash);g-=h;raise"bad route"if !u=c.urls.find{|x|break x if
x.scan(p).size==g.size&&/^#{x}\/?$/=~(x=g.inject(x){|x,a|x.sub p,U.escape((a.
to_param rescue a))}.gsub(/\\(.)/){$1})};h.any?? u+"?"+U.build_query(h[0]):u
end;def / p;p[0]==?/?@root+p : p end;def URL c='/',*a;c=R(c,*a) if c.respond_to?(
:urls);c=self/c;c=@request.url[/.{8,}?(?=\/|$)/]+c if c[0]==?/;URI c end end
module Base;attr_accessor:env,:request,:root,:input,:cookies,:state,:status,
:headers,:body;T={};L=:layout;def lookup n;T.fetch(n.to_sym){|k|t=Views.
method_defined?(k)||(t=O[:_t].keys.grep(/^#{n}\./)[0]and Template[t].new{
O[:_t][t]})||(f=Dir[[O[:views]||"views","#{n}.*"]*'/'][0])&&Template.
new(f,O[f[/\.(\w+)$/,1].to_sym]||{});O[:dynamic_templates]?t: T[k]=t} end
def render v,*a,&b;if t=lookup(v);r=@_r;@_r=o=Hash===a[-1]?a.pop: {};s=(t==true)?mab{
send v,*a,&b}: t.render(self,o[:locals]||{},&b);s=render(L,o.merge(L=>false)){s
}if o[L]or o[L].nil?&&lookup(L)&&!r&&v.to_s[0]!=?_;s;else;raise"no template: #{v}"
end;end;def mab &b;extend(Mab);mab(&b) end;def r s,b,h={};b,h=
h,b if Hash===b;@status=s;@headers.merge!(h);@body=b end;def redirect *a;r 302,
'','Location'=>URL(*a).to_s end;def r404 p;P%"#{p} not found"end;def r500 k,m,e
raise e end;def r501 m;P%"#{m.upcase} not implemented"end;def serve(p,c)
(t=Rack::Mime.mime_type p[/\..*$/],Z)&&@headers[E]=t;c;end;def to_a;@env[
'rack.session'][SK]=Hash[@state];r=Rack::Response.new(@body,@status,@headers)
@cookies._n.each{|k,v|r.set_cookie k,v};r.to_a end;def initialize env,m
r=@request=Rack:: Request.new(@env=env);@root,@input,@cookies,@state,@headers,
@status,@method=r.script_name.sub(/\/$/,''),n(r.params),Cookies[r.cookies],
H[r.session[SK]||{}],{E=>Z},m=~/r(\d+)/?$1.to_i: 200,m;@cookies._p=self/"/" end
def n h;Hash===h ?h.inject(H[]){|m,(k,v)|m[k]=
n(v);m}: h end;def service *a;r=catch(:halt){send(@method,*a)};@body||=r;self
end end;module Controllers;@r=[];class<<self;def R *u;r=@r;Class.
new{meta_def(:urls){u};meta_def(:inherited){|x|r<<x}}end;
def v;@r.map(&:urls);end;def D p,m,e;p='/'if
!p||!p[0];(a=O[:_t].find{|n,_|n==p}) and return [I,:serve,*a]
@r.map{|k|k.urls.map{|x|return(k.method_defined? m)?[k,m,*$~[1..-1].map{|x|U.unescape x}]:
[I, 'r501',m]if p=~/^#{x}\/?$/}};[I,'r404',p] end;
module F;A= ->(c,u,p){u.prepend("/"+p) unless c.to_s == "I"}end;N=H.new{|_,x|x.downcase}.
merge!("N"=>'(\d+)',"X"=>'([^/]+)',"Index"=>'');def M(pr);def M(pr);end;constants.
map{|c|k=const_get(c);k.send:include,C,X,Base,Helpers,Models
@r=[k]+@r if @r-[k]==@r;k.meta_def(:urls){[F::A.(k,"#{c.to_s.scan(/.[^A-Z]*/)
.map(&N.method(:[]))*'/'}",pr)]}if !k.respond_to?:urls}end end;I=R()end;X=
Controllers;class<<self;def routes;X.M O[:url_prefix];(Apps.map(&:routes)<<X.v).flatten;end;
def call e;X.M O[:url_prefix];k,m,*a=X.D e["PATH_INFO"],e['REQUEST_METHOD'].
downcase,e;k.new(e,m).service(*a).to_a;rescue;r500(:I,k,m,$!,:env=>e).to_a end
def method_missing m,c,*a;X.M O[:url_prefix];h=Hash===a[-1]?a.pop: {};e=H[Rack::MockRequest.
env_for('',h.delete(:env)||{})];k=X.const_get(c).new(e,m.to_s);h.each{|i,v|k.
send"#{i}=",v};k.service(*a) end;def use*a,&b;m=a.shift.new(method(:call),*a,&b)
meta_def(:call){|e|m.call(e)}end;def pack g, *a, &b;G<<g;include g;
extend g::ClassMethods if defined?(g::ClassMethods);g.setup(self)if g.respond_to?(:setup)end;
def gear;G end;def options;O end;def set k,v;O[k]=v end
def goes m,g=TOPLEVEL_BINDING;Apps<<a=eval(S.gsub(/Camping/,m.to_s),g);caller[0]=~/:/
IO.read(a.set:__FILE__,$`)=~/^__END__/&&(b=$'.split /^@@\s*(.+?)\s*\r?\n/m).shift rescue nil
a.set :_t,H[*b||[]];end;end
module Views;include X,Helpers end;module Models;autoload:Base,'camping/ar'
Helpers.send:include,X,self end;autoload:Mab,'camping/mab'
autoload:Template,'camping/template';C end

Because Camping is intended to be as compact as possible, single file apps are encouraged. Which is pretty cool.

Camping.goes :Nuts

module Nuts
  module Controllers
    class Index
      def get
        render :index
      end
    end
  end
  module Views
    def index
      h1 'Hello World'
    end
  end
end

Also because single file apps are a thing, more than one "app" can be mounted together:

Camping.goes :Nuts # Nuts app is mounted.
# ... Nuts module here

Camping.goes :Blog # Blog app is mounted.
# ... Blog module here

A Camping app is just a module mounted within a namespace, either the global namespace, or another app's namespace. This makes distributing an app via RubyGems pretty easy too.

Camping's default view architecture uses MAB which is an HTML generator in Ruby.

module Views
  def index
    if @posts.empty?
      h2 'No posts'
        p do 
          text 'Could not find any posts. Feel free to '
          a 'add one', :href => R(PostNew)
          text ' yourself'
        end
    else
      @posts.each do |post|
        _post(post)
      end
    end
  end
end

I've been updating Camping to have better database support. It comes with built in support for ActiveRecord, but what about other database engines? Through a new plugin system called gear, I'm adding support for different database systems. In the next version the built in ActiveRecord integration is being moved to some Camping Gear named GuideBook:

require 'camping'
require 'guidebook' # Guidebook adds ActiveRecord to Camping.

Camping.goes :Nuts

module Nuts

  # In Camping you Pack Gear.
  pack Camping::GuideBook

  module Models
    class Page < Base; end
  end

  def self.create
    # establishes a database connection when the app is created.
    establish_connection()
  end
  # ... Controllers, Views, Helpers.
end

Camping has some tricks up it's sleeves. Like adding styles and other files to the end of your camping code:

# ... The rest of your camping app up here

__END__

@@ /style.css
* { margin: 0; padding: 0 }

Camping is based on Rack so adding middleware is pretty simple:

require 'camping'
require 'warden'

module Nuts
  
  use Warden::Manager do |manager|
    manager.default_strategies :password, :token
    manager.failure_app = BadAuthenticationEndsUpHere.new
  end
  
end

What's next for Camping?

Camping is mostly just plain old ruby with some funny bits. But it's so interesting how all the pieces fit together. It's so small and compact you can read the whole source code in a few minutes, but it's packed with interesting Ruby tricks that truly stretch what you can do with Ruby.

I began looking for a new web framework in early 2022, I found Camping to be interesting and exactly what I was looking for. Small, Simple, and stuck in a single file. Unmaintained for 6 years it didn't work right away, but after a little bug fixing it was good as new. Working on Camping has taught me so much about Ruby. It's absolutely been worth every moment.

I think that the future of Camping is to keep the core framework as small as possible, but make it easy to extend. Things like new view systems, database ORMs, or even websockets. The next version of Camping will have a decoupled, yet improved database plugin called Guidebook, A Camping NEW command line helper, and documentation improvements. Soon I'll add support for websockets, Phlex Views, the Falcon webserver, Rack3, and the Sequel database toolkit, configurations using a kdl config file, and asynchronous code execution. In particular I'm very interested in making the developer experience as fantastic as possible. To me that means inspection and testing tools. Amazing and detailed Documentation. Plus common helpers to make difficult tasks very simple to understand and perform.

Working on Camping has made me very excited about my career again. I really love it.