Finalist CTO-board Project: Persia
De afgelopen maand ben ik druk bezig geweest met een project om een nieuwe view-laag de schrijven voor RubyOnRails. Lees hieronder hoe dat verlopen is.
Wat is een CTO-board project?
Finalist heeft sinds een klein jaar een CTO-board, dat gekozen wordt door alle ontwikkelaars.
Dit CTO-board bestaat uit twee personen, en heeft als doel het evalueren van nieuwe technologieën. Daarnaast sponsort het twee research-projecten per jaar. Per project is vier weken ontwikkeltijd ingeruimd.
Wat wilde ik bereiken?
Hoewel Finalist vooral een Java-partij is, krijgt ook Ruby steeds meer aandacht, vooral vanwege Ruby On Rails. Aangezien we vooral web-applicaties maken, is de belofte van hogere productiviteit natuurlijk erg interessant. Ik bewonder het model-gedeelte (de M uit het Model-View-Controller pattern), dat in Ruby ‘Active Record’ heet, evenals de totale integratie van een Rails-project (testen, deployen, build-scripts, versie-beheer van het database-schema, het zit er allemaal standaard in). Het geheel is meer dan de som van de delen.
Wat ik niet zo mooi vond, was de view-laag. De view-laag is gebaseerd op ‘erb’, een soort Java Server Pages: HTML vermengd met Ruby expressies en statements, aangevuld met ‘helpers’, methodes die HTML genereren. Een voorbeeld van een typisch fragment is bijvoorbeeld dit:
<%= link_to :action => 'edit' %>
Met deze benadering is het niet meer mogelijk om je HTML-code in een browser te previewen, of aan te passen in een editor zoals DreamWeaver. En als de HTML-code door web-designers wordt aangeleverd, moeten de ontwikkelaars dit converteren naar een template-formaat. Dat is een vervelend werkje en toekomstige veranderingen in de HTML moeten handmatig samengevoegd worden in de template code.
In Java zijn er sinds enkele jaren frameworks beschikbaar die een andere benadering hebben. Frameworks zoals Tapestry en Wicket proberen de HTML zoveel mogelijk onaangetast te laten. Ze gebruiken deze HTML als templates en stellen pagina’s runtime samen via deze templates, Java-code en herbruikbare componenten.
Ook in Ruby waren er alternatieven beschikbaar, bijvoorbeeld Kwartz, Amrita2 en MasterView.
Amrita2 en Kwartz gebruiken (misbruiken) HTML id tags om posities in de DOM te markeren. Kwartz gebruikt presentatie-logica in een nogal breedsprakige taal die een mix tussen Ruby en CSS lijkt, en compileert de templates naar erb. Amrita2 gebruikt geen logica maar data om de output te genereren - elegant, maar het beperkt je aanzienlijk in je mogelijkheden. MasterView gebruikt attributes in een aparte namespace, en stopt alle logica in de attributen zelf, waardoor je weer logica in je templates krijgt, en minder flexibel bent dan met gescheiden view-logica.
Deze benaderingen voldeden allemaal niet aan mijn eisen: zo schoon mogelijke HTML, een krachtige view-laag waarmee alles mogelijk is, en die tevens zeer bondig is.
Wat waren mijn doelen?
Mijn voornaamste doel was het schrijven van een DSL (Domain Specific Language) om de DOM van een XHTML pagina te manipuleren. Daarnaast moest deze DSL geïntegreerd worden in Rails, wilde ik de standaard scaffolding van Rails converteren zodat het mijn view-model gebruikte, en wilde ik enkele helper-methodes toevoegen die specifiek voor Rails waren (zoals het automatisch vullen van forms). Daarnaast wilde ik ruimschoots aandacht besteden aan documentatie, testen en performance.
Wat is er opgeleverd?
Wat ik uiteindelijk heb opgeleverd is te vinden op Rubyforge. Het bestaat uit vier onderdelen: de kern, de scaffold-generator, een voorbeeld project dat Persia gebruikt, en een uitbreiding op het test-framework dat ik heb gebruikt.
Persia
De kern bestaat uit drie onderdelen: een lightweight Element class die zich kan aanmaken via een REXML document (Ruby’s standaard XML parser). Deze class bevat methoden om de DOM-tree te veranderen, zichzelf te renderen, en kan zichzelf cachen. Verder herkent het de speciale attributen die door Persia gebruikt worden.
Het tweede onderdeel is de Cursor, die naar een bepaald element wijst. De Cursor is de class waartegen de ontwikkelaar praat, en heeft een uitgebreide API voor het zoeken in en manipuleren van elementen.
Tenslotte is er de integratie met Rails. Hieronder valt het aanpassen van de controller base class van Rails (zodat Rails onze view-laag ook echt gebruikt), en het inladen en parsen van de HTML-files.
Persia_generator
De generator van Persia genereert scaffolding, net als Rails, maar nu met onze view-laag. De verschillen zijn dat er geen ‘rhtml’-files worden aangemaakt, maar ‘mdml’-files (Modifiable Document Markup Language) - kortom, pure XHTML met enkele extra attributes in een aparte namespace. Ook wordt er een view-class gecreëerd, waarin de ‘mdml’-resources daadwerkelijk ingevuld worden.
Voorbeeld project
Het voorbeeld-project is bedoeld om een idee te krijgen van hoe een applicatie die Persia gebruikt eruitziet. Het bevat een gegenereerde scaffold, en een grote Wikipedia-pagina, die runtime met data uit een database wordt gevuld. Voor de scaffolding is er een test beschikbaar die de browser (Internet Explorer) door de hele scaffold heenstuurt.
Spekmachine
Voor het schrijven van tests heb ik gekozen voor het RSpec framework. Hierbij schrijf je geen tests, maar specificaties. Je definiëert een context (bv. een enkel element met twee sub-elementen) en schrijft dan de specificaties voor die context. Bijvoorbeeld:
@element.should_have(2).children
Je zorgt zo voor een strikte scheiding tussen je asserts en de overige test-code. Daarnaast krijg je veel beter leesbare testrapporten door de should notatie. @element.should_be_empty is bijvoorbeeld beter leesbaar dan assert @element.empty?.
Een nadeel van deze methode (hetzelfde geldt overigens voor Test::Unit) is dat het slecht werkt als het opbouwen vanaf nul van een context een dure operatie is. Als je in een web-applicatie het uitschrijven van een gebruiker wil testen, is het vervelend als je je daarvoor eerst moet registreren en inloggen. Het zou mooi zijn als bij het registreren meteen de bijbehorende specificaties getest werden.
Met dat doel is Spek Machine geschreven. Hierin beschouwen we elke context als een ’state’, en definiëren we per context transities naar andere contexten. De specificatie-runner leest alle contexten en transities in, bouwt een gerichte graaf op, en zoekt het korste pad waarbij alle transities en contexten doorlopen worden.
Hoe werkt Persia? Control Flow
Wanneer een http-request bij de dispatcher van Rails aankomt, worden eerst met de Routes module de controller en de action bepaald. Vervolgens wordt een nieuwe controller geïnstantiëerd, en de betreffende action-methode aangeroepen. Als in die methode een expliciete call naar render wordt gedaan, wordt die uitgevoerd, anders wordt render_action aangeroepen met de naam van de huidige action. In render_action wordt gekeken of er een view-class bestaat voor de huidige controller.
De eerste keer wordt de view-class geladen, en laadt de view-class alle resources die in zijn directory staan, plus eventuele expliciet gedefiniëerde resources. De resources (mdml-files) worden door een XML-parser verwerkt, en vervolgens geconverteerd naar Elements en geïndexeerd op basis van id.
Als de view-class gevonden is, wordt een nieuw view-object geïnstantiëerd. De instance-variabelen uit de controller worden naar de view gekopiëerd, en de action-methode in de view wordt aangeroepen met een cursor naar een leeg document. In de view wordt het document via de cursor gevuld (zie beneden). Vervolgens wordt het document gerenderd en weggeschreven naar de response.
Hoe werkt Persia? Dom-manipulatie
Documenten worden gemanipuleerd via de Cursor. De cursor wijst naar een bepaald element, naar een lijst elementen, naar een textnode of naar een attribute. Eenvoudige operaties zijn onder andere het vervangen van een element door een resource, het zetten van, toevoegen aan of veranderen van een attribute of textnode. Complexe operaties zijn bijvoorbeeld each, map en code>zip (zie handleiding). Verder kun je via de cursor op verschillende manieren navigeren door een document. Je kunt een element met een bepaald id zoeken, je kunt alle sub-elementen van een bepaald type selecteren, of een sub-element met een gegeven index kiezen.
Voorbeelden code
cursor.text.set 'hallo' cursor.text.append 'hallo' cursor[:class].set 'active' cursor[:class].append 'node' cursor.table.tr[3].td[1].text.set 'Finalist' cursor.find(:title).set 'Welkom op onze website' cursor.replace :document, :content => :contact_info cursor.tr.each {|cur| cur[:class].set 'row' } cursor.find(:row).map(1..10) do |cur,x| cur[:class].set ['even','odd'][x % 1] end
Specifications met RSpec
Alle tests zijn geschreven met behulp van RSpec. In een spec-file definiëer je een aantal contexts, en in elke context schrijf je een setup block, en een aantal specificaties. Hieronder zie je een voorbeeld. Zie de RSpec website voor meer informatie.
Voorbeelden
context "Div element with attributes and textnode" do setup do @xml = '<div>mytext</div>' @cursor = to_cursor @xml end specify "cursor is at root" do @cursor.should_be_root end specify "cursor has one child" do @cursor.current_element.should_have(1).children end specify "cursor has textnode" do @cursor[].should_be_text @cursor[].to_s.should_be_equal 'mytext' end specify "cursor has attribute" do @cursor[:onclick].should_be_attribute @cursor[:onclick].to_s.should_be_equal 'foo()' end specify "cursor has no attribute onload" do @cursor[:onload].should_be_attribute @cursor[:onload].to_s.should_not_be end end
Specificaties met Spek Machine
Voor de door de scaffold gegenereerde code in het voorbeeld-project heb ik specificaties geschreven voor de Spek Machine. Deze specificaties gebruiken de Watir-library om Internet Explorer te scripten. Hier volgt een voorbeeld:
Voorbeelden
setup :index_nologin do $ie = Watir::IE.start("http://localhost:3000/editor/") end context :index_nologin, "index - not logged in" do specify "contains form" do $ie.form(:name, 'loginform').should_exists end # fill u/p with correct values, submit goto :index_empty do $ie.text_field(:name, "login").set("admin") $ie.text_field(:name, "password").set("welkom1") $ie.button(:value, "Inloggen").click end goto :index_wronglogin do $ie.text_field(:name, "login").set("foo") $ie.text_field(:name, "password").set("bar") $ie.button(:value, "Inloggen").click end end
Hoe nu verder?
De grootste test die Persia nu wacht, is dat het daadwerkelijk gebruikt gaat worden. Ongetwijfeld komen er dan nog genoeg problemen naar boven. Daarnaast moet de documentatie nog verbeterd worden, moeten er meer specificaties geschreven worden, en kan het zeker geen kwaad om de performance eens goed door te testen. Kortom, het wordt tijd om Persia te installeren, en dat gaat als volgt:
gem install persia
Succes!
———————————————————————————–
Meer weten over Ruby-specialist Finalist IT Group?


