proto prototype

Geplaatst door Remco van 't Veer do, 12 okt 2006 00:12:00 GMT

Laatst sprak ik iemand die rails gebruikt om prototypes te bouwen voor J2EE web applicaties omdat het zo lekker snel werkt. Het uiteindelijke project wordt uitgewerkt in J2EE. Ten opzichte van J2EE is er met rails erg snel iets in elkaar te zetten maar wat doe je als je een prototype nodig hebt voor een rails site?

Je kan natuurlijk gewoon beginnen met rails fancy_shit en los gaan. Vroeg in de ontwerp fase van een project vind ik dat een beetje overkill, als je alleen maar een beetje wilt goochelen met een paar objectjes om gevoel te krijgen voor het probleem wat je probeert op te lossen.

versus

Gelukkig is er iets veel beters dan rails voor dit soort dingen!

Camping is klein web framework gemaakt door een van de grootste helden uit de Ruby gemeenschap, Why The Lucky Stiff. Het past MVC, kent ActiveRecord, doet Markaby en is reloadable (ofwel edit je app en druk op reload in je browser), maar belangrijker nog geeft me de mogelijkheid om in een enkele soepele beweging een web appje uit m’n mouw te schudden!

Wat hebben we allemaal mee gekregen? Laten we beginnen bij de C van Controller, wat heb je immers aan een web app als je geen browser requests kan verwerken. Daar gaan we:

gem install camping

En ons eerste appje:

require 'camping'
Camping.goes :Proto

module Proto
  module Controllers
    class Index < R '/'
      def get
        'wij gaan kamperen!'
      end
    end
  end
end

Dat is alles! Een complete applicatie welke “wij gaan kamperen!” roept. Opslaan in een file met naam proto.rb, opstarten met camping proto.rb, dan je browser naar http://localhost:3301/ en tevreden toekijken.

Ons appje heeft een controller Index welke gekoppeld is aan '/'. Voor GET requests op deze locatie wordt de tekst “wij gaan kamperen!” terug geleverd. Maar dat was duidelijk toch?

Laten we '/' uitbreiden zodat we gegevens aan de controller kunnen doorgeven:

  module Controllers
    class Index < R '/(.*)'
      def get(arg)
        arg = 'kamperen' if arg.empty?
        "wij gaan #{arg}!" 
      end
    end
  end

Nu kunnen we naast kamperen ook gaan fietsen!

Het argument aan R wordt geïnterpeteerd als reguliere expressie waarvan eventuele groepen (in reguliere expressie aangegeven met haakjes) doorgegeven worden als argumenten voor de controller methode. Een echte expressie doorgeven mag ook maar '/(.*)' is beter leesbaar dan /\/(.*)/ of %r{/(.*)} volgens mij.

Het resultaat van het laatste statement (return value dus), wordt gerenderd. Maar we kunnen het ook een beetje op leuken met markaby:

  module Controllers
    class Index < R '/(.*)'
      def get(arg)
        arg = 'kamperen' if arg.empty?
        @mission = "wij gaan #{arg}!" 

        html do
          head { title @mission }
          body { h1 @mission }
        end
      end
    end
  end

en dat kunnen we verhuizen naar de V van View:

  module Controllers
    class Index < R '/(.*)'
      def get(arg)
        arg = 'kamperen' if arg.empty?
        @mission = "wij gaan #{arg}!" 
        render :index
      end
    end
  end

  module Views
    def index
      html do
        head { title @mission }
        body do
          h1 @mission
          text 'hallo uit de view!'
        end
      end
    end
  end

en dat kan zelfs beter met de magische layout method:

  module Views
    def layout
      html do
        head { title 'hallo uit de layout!' }
        body { self << yield }
      end
    end

    def index
      h1 @mission
      text 'hallo uit de view!'
    end
  end

En meerdere pagina’s? Hoe knoop je die dan aan elkaar? Hebben we helper methoden zoals url_for? Natuurlijk! De constructie:

class Index < R '/'

heeft z’n tegenpool voor de view:

a 'index', :href => R(Index)

dit levert een URL naar de gewenste controller. In actie:

module Proto
  module Controllers
    class Index < R '/'
      def get
        h1 'index'
        a 'detail', :href => R(Detail)
      end
    end

    class Detail < R '/detail'
      def get
        h1 'detail'
        a 'index', :href => R(Index)
      end
    end
  end
end

De index pagina linkt naar detail en anders om.

Nu met objectjes goochelen, een simpele site met artikelen erop. Nee, een volledig CMS!

module Proto
  ARTICLES = []

  module Controllers
    class Index < R '/'
      def get; render :index; end
    end

    class Add < R '/add'
      def get; render :add; end
      def post
        ARTICLES << {
          :title => input.title,
          :body => input.body
        }
        redirect Index
      end
    end
  end

  module Views
    def layout
      html do
        head { title 'a proto-prototype' }
        body { self << yield }
      end
    end

    def index
      a 'nieuw artikel', :href => R(Add)
      ARTICLES.each do |article|
        h1 article[:title]
        text article[:body]
      end
    end

    def add
      form :method => 'POST' do
        input :type => 'text', :name => 'title'
        br
        textarea :name => 'body', :rows => 10
        input :type => 'submit'
      end
    end
  end
end

Interessant in deze versie van ons appje is de post methode in de Add controller. Ten eerste de naam van de methode: post, yep hier worden alle POST requests afgewerkt. Geen gemier met request.post? of speciale routing, GET en POST hebben gewoon hun eigen methoden. Verder wordt de input methode gebruikt om request parameters op te pakken en wordt de redirect methode gebruikte om weer terug naar de begin pagina te springen.

Alle artikelen worden bewaard in een array genaamd ARTICLES en artikelen zijn niet meer dan een hash met twee velden; title en body. Dat is genoeg voor een klein experimentje maar zodra je wat aan je applicatie verandert ben je je data kwijt, niet zo handig. Gelukkig hebben we de M nog van Model.

Zoals ik al eerder aangaf kan je op de camping met ActiveRecord spelen. Standaard gebeurt dat in een SQLite database die je niet zelf aan hoeft te maken. Lees eerst de waarschuwing van _why over de installatie van de sqlite3-ruby gem als je SQLite nog niet in combinatie met Ruby gebruikt hebt.

Laten we ons CMS persistent maken! Een PCMS! Eerst introduceren we models met migratie script (ja echt!) in onze Proto module:

module Proto
  module Models
    class Article < Base; end

    class BasicVersion < V 1.0
      def self.up
        create_table :proto_articles do |t|
          t.column :id,    :integer
          t.column :title, :string
          t.column :body,  :text
        end
      end
      def self.down
        drop_table :proto_articles
      end
    end
  end

  def self.create
    Models.create_schema
  end
..

De self.create methode verzekert dat de tabellen ook echt aangemaakt worden, deze methode wordt altijd bij het laden van je appje aangeroepen. De naam BasicVersion heb ik zo uit m’n duim gezogen, het is gewoon de naam van die migratie. De namen van de tabellen worden geprefixed met de naam van je app, platgeslagen.

Nu kunnen we de ARTICLES array weg halen, in de post methode van de Add controller kunnen we nu Article.create!(input) schrijven en in de index view kan ARTICLES.each vervangen worden door Models::Article.find(:all).each. Reload in de browser et voilá een PCMS!

Reload doet migraties?? Ja zeker! Voeg maar eens een migratie toe:

..
    class TimeStamped < V 1.1
      def self.up
        add_column :proto_articles, :created_at, :datetime
      end
      def self.down
        remove_column :proto_articles, :created_at
      end
    end
..

en een maak created_at zichtbaar in de index view:

..
    def index
      a 'nieuw artikel', :href => R(Add)
      Article.find(:all).each do |article|
        h1 article.title
        p article.created_at
        p article.body
      end
    end
..

Nieuw artikeltje maken, kijken in het overzicht en tevreden toekijken. Misschien is die wel een leuk idee voor een rails plugin, reloadable-migrations-in-development?

Wat hebben we nog meer in onze knapzak zitten? Hmm, M van Model hebben we gehad, V van View ook behandelt en C van Controller volgens mij ook.. hoewel hoe zit het eigenlijk met sessies? Het kan toch niet zo zijn deze ik zelf cookie headers moet gaan zetten?

Tuurlijk niet! Als je sessies echt nodig hebt kan je ze gewoon meenemen net als een opblaasboot.

require 'camping/session'
..

Een uitbreiding voor je create method:

..
  def self.create
    Camping::Models::Session.create_schema
    Models.create_schema
  end
..

En het in mixen van sessies:

..
module Proto
  include Camping::Session
..

Nu heb je in je controllers een @state variable voor elke bezoeker, te gebruiken als hash. Eindelijk kunnen onze PCMS beschermen tegen mensen die helemaal niets te melden hebben!

Een nieuwe controller:
..
    class Login < R '/login'
      def get
        form :method => 'post' do
          label 'wachtwoord'
          input type => 'password', :name => 'password'
          input type => 'submit'
        end
      end
      def post
        if input.password == 'douwe dabbert'
          @state[:login] = true
          redirect Add
        else
          h1 'no passeran'
        end
      end
    end
..

En de add controller beschermen we met:

..
    class Add < R '/add'
      def get
        if @state[:login]
          render :add
        else
          redirect Login
        end
      end
      def post
        Article.create!(input) if @state[:login]        
        redirect Index
      end
    end
..

Nou, ik zou zeggen “enterprise ready”! Een SPCMS klaar voor ISO certificering! Download de code en doe er je voordeel mee!

Camping is wat mij betreft een vonkel in de robijn en dat in minder dan 4k! In veel opzichten vind ik het veel prettiger werken dan rails; migraties tijdens reload, automatische creatie van je database, alles in een enkele file, alles kort en bondig.. Ik zal het nog een keer zeggen _why is mijn grote held!

Geplaatst in  | 3 reacties

Reacties

  1. Erik van Oosten zei 60 dagen later:

    Sqlite werkte bij mij pas nadat ik in de index method niet Article.find(:all).each maar Models::Article.find(:all).each schreef. Wat deed ik fout?

  2. Erik van Oosten zei 60 dagen later:

    Ik ben er nu achter dat je eigenlijk @articles moet initialiseren in de controler. Deze kun je dan gebruiken in de view.

  3. Remco zei 60 dagen later:

    Oeps, sorry.. Ik dacht dat ik echt alle code getest had. Blijkbaar wordt Modules wel in automatisch Controllers geinclude maar niet in je Views.

(Laat url/e-mail achter »)

   Voorvertoning