Wednesday, December 30, 2009

Migrer vers ruby 1.9

Je viens de migrer deux applications Rails 2.3.5 vers Ruby 1.9.1 avec deux problèmes majeurs concernant les encodings. Bien que Ruby soit unicode par défaut, pas mal de gems ne parlent pas l'UTF-8 par défaut...

Premier soucis avec ERB qui compile les templates en utilisant ASCII-8BIT par défaut. Cela génère l'erreur suivante:
ActionView::TemplateError (incompatible character encodings: ASCII-8BIT and UTF-8)

Il y a pas mal de discussions sur le sujet ici: https://rails.lighthouseapp.com/projects/8994/tickets/2188-i18n-fails-with-multibyte-strings-in-ruby-19-similar-to-2038

Une solution hacky à coup de monkey patch est de créer un fichier lib/actionview_utf8 comme ceci (c'est la ligne avec le source.force_encoding qui est rajoutée au code original):

module ActionView
  module Renderable #:nodoc:
    private
      def compile!(render_symbol, local_assigns)
        locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join

        source = <<-end_src
          def #{render_symbol}(local_assigns)
            old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
          ensure
            self.output_buffer = old_output_buffer
          end
        end_src
        source.force_encoding(Encoding::UTF_8) if source.respond_to?(:force_encoding)

        begin
          ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
        rescue Errno::ENOENT => e
          raise e # Missing template file, re-raise for Base to rescue
        rescue Exception => e # errors from template code
          if logger = defined?(ActionController) && Base.logger
            logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
            logger.debug "Function body: #{source}"
            logger.debug "Backtrace: #{e.backtrace.join("\n")}"
          end

          raise ActionView::TemplateError.new(self, {}, e)
        end
      end
  end
end

puis d'ajouter les lignes suivantes à environnement.rb:

Rails::Initializer.run do |config|
  ...
  config.after_initialize do 
    require 'lib/actionview_utf8'
  end
end

Ensuite je suis tombé sur le problème des adaptateurs aux bases de données qui retournent de l'ASCI-8BIT au lieu d'UTF-8 à la lecture des données. Une solution du même acabit est décrite ici: http://gnuu.org/2009/11/06/ruby19-rails-mysql-utf8/. Ceci dit ça ne fonctionne qu'avec MySql.

Donc on créé le fichier lib/mysql_utf8:

require "mysql"

class Mysql::Result
  def encode(value, encoding = "utf-8")
    String === value ? value.force_encoding(encoding) : value
  end

  def each_utf8(&block)
    each_orig do |row|
      yield row.map {|col| encode(col) }
    end
  end
  alias each_orig each
  alias each each_utf8

  def each_hash_utf8(&block)
    each_hash_orig do |row|
      row.each {|k, v| row[k] = encode(v) }
      yield(row)
    end
  end
  alias each_hash_orig each_hash
  alias each_hash each_hash_utf8
end

et pour environnement.rb on se retrouve avec:

Rails::Initializer.run do |config|
  ...
  config.after_initialize do 
    require 'lib/actionview_utf8'
    require 'lib/mysql_utf8'
  end
end

ouf, tous les tests passent...

Tuesday, December 29, 2009

Zend Framework avec lighthttpd

Je vais travailler sur un projet en PHP avec Zend Framework. Venant du monde Rails, on s'habitue assez vite à lancer son serveur web dans un terminal, voir plusieurs en parallèle sans avoir à manipuler la configuration d'un Apache qui tourne sous un autre compte utilisateur.

En PHP je n'ai pas trouvé d'équivalent à Webrick ou Mongrel intégré à ZF, mais je m'en suis finalement sorti avec lighthttpd.

A la racine du projet ZF, créez un lighthttpd.conf dans lequel vous configurez entre autres:
  • le port 3000 (on ne change pas de vieilles habitudes :)
  • l'URL rewriting pour que toutes les requêtes passent par index.php (l'équivalent du .htaccess tel que créé par ZF)
  • l'accès à votre application via FastCGI


ce qui donne quelque chose comme suit:


server.document-root = env.PWD + "/public"
server.port = 3000

server.modules = (
  "mod_rewrite",
  "mod_fastcgi"
)


mimetype.assign = (
  ".html" => "text/html", 
  ".txt" => "text/plain",
  ".jpg" => "image/jpeg",
  ".png" => "image/png" 
)

url.rewrite-once = (
  ".*\?(.*)$" => "/index.php?$1",
  ".*\.(js|ico|gif|jpg|png|css)$" => "$0",
  "" => "/index.php"
)

static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )

server.indexfiles = ( "index.php" )

fastcgi.server = (
  ".php" =>
    ( "localhost" =>
      (
        "socket" => "/tmp/php-fastcgi.socket",
        "bin-path" => "/usr/bin/php-cgi"
      )
    )
)

Ensuite un petit script bash pour nous faciliter le lancement du serveur et l'initialisation de l'environnement ZF, par exemple start_zf.sh.

#!/bin/sh
export APPLICATION_ENV=development
lighttpd -f lighthttpd.conf -D

N'oubliez pas de rendre le script exécutable:
chmod +x start_zf.sh

puis lancez l'accès à votre projet ZF:

./start_zf.sh

Si tout va bien le site est accessible sur http://localhost:3000

Friday, December 18, 2009

SimpleWebDoc et yUML

Je viens d'interfacer SimpleWebDoc et yUML pour générer les diagrammes d'héritage entre les classes d'un même package. Le tout à un clic de souris.

Le package Tests:



Le package Kernel:



Saturday, December 12, 2009

SimpleWebDoc sur SqueakSource

Suite à mon billet précédent, Pharo et documentation des packages, j'ai continué a explorer Pharo pour ressortir les méthodes des classes et les commentaires contenus en en-tête des méthodes.

J'ai mis le package SimpleWebDoc sur SqueakSource. Cela me donne l'occasion d'expliquer comment charger un package depuis Monticello. Tout d'abord lancez le Monticello Browser depuis le menu World.




Cela ouvre une fenêtre dans laquelle on voit les packages installés dans le panneau de gauche, les repository (dépôts) dans le panneau de droite. L'étape suivante est d'ajouter le repository où se trouve SimpleWebDoc dans la liste de ceux enregistrés dans Monticello. Cliquez sur le bouton +Repository et sélectionnez HTTP (Note: un repository Monticello est un simple répertoire local ou distant où se trouve les packages zippés, avec l'extension .mcz).



Saisissez ensuite l'emplacement du repository, ici: http://www.squeaksource.com/SimpleWebDoc




Le repository ajouté, sélectionnez-le et cliquez Open.



Une fenêtre s'ouvre listant tous les packages présents dans le repository (panneau de gauche) et la liste des versions disponibles (panneau de droite), la plus récente en tête. Sélectionnez la version la plus récente et cliquez sur Load pour charger le package SimpleWebDoc.



SWDocView est automatique enregistré comme application Seaside au chargement. Si vous regardez la méthode de classe initialize de SWDocView:


initialize
  self registerAsApplication: 'view-doc'

Une classe est un objet, et comme tout objet son instanciation appelle initialize. En chargeant le package SimpleWebDoc, on instancie la meta-classe SWDocView (elle même instance de MetaClass). SWDocView s'enregistre alors comme application nommée view-doc.


Vous pouvez donc ouvrir votre navigateur web préféré et aller à l'adresse:




Les méthodes de classe sont en italique et soulignées:



Thursday, December 10, 2009

The Smalltalk way to learn Smalltalk

Pour apprendre un nouveau langage, avant de me jeter sur le compilateur / interpréteur et tenter d'écrire un (mauvais) programme, j'achète un ou deux livres bien perçus pour m'imprégner de la philosophie et objectifs du langage. Je lis quelques programmes.

Pour Smalltalk je crois avoir commis une erreur et j'ai mis du temps à tout simplement "rentrer dedans", bien plus que tous les langages profondément objets étudiés auparavant (dont Ruby ).

I'm still amazed by how many people think they can grok Smalltalk by seeing syntax examples. Smalltalk isn't its syntax, it's its environment. Smalltalk is a living world of running objects, there are no files, no applications, just what's running. To understand Smalltalk, you have to either actually use it for a while, or have a seasoned Smalltalker demonstrate it to you. Reading sample code just won't cut it.
(Ramon Leon, Why Smalltalk)

C'est un vrai conseil ;)

Et je comprends maintenant ceci: Object oriented programming means something entirely different to a Smalltalker than to someone doing OO in another language.

Au passage, le blog de Ramon Leon OnSmalltalk est une vrai mine d'or, articles complets, bien écrits, profonds et les commentaires n'en valent pas moins.

Sunday, December 6, 2009

Pharo et documentation des packages

Voici quelques expérimentations faites alors que je voulais explorer les documentations contenues dans Pharo. Dans ce billet nous verrons comment ressortir les commentaires des classes d'un package donné. Puis je décrirai le codage d'une application Seaside minimaliste pour visualiser cette documentation depuis un navigateur Web.

1. Voyage au centre du package

Les classes sont regroupées dans des packages gérées par Monticello. On peut faire l'analogie avec les gems du monde Ruby ou les gestionnaires de paquets des distributions Linux.

Chaque package étant une instance de la classe MCPackage, on peut en sortir la liste. Ouvrez un Workspace et affichez le résultat du code suivant (via click-droit, print it)

MCPackage allInstances



Cela devrait vous retourner une longue liste de packages:



Comptons en le nombre en évaluant la taille du tableau retourné:

MCPackage allInstances size.

Sur mon image je trouve 155 packages.

Chaque package a un nom via l'accesseur name. On peut ainsi en sortir la liste:

MCPackage allInstances collect: [:p| p name]

En triant par ordre alphabétique:

(MCPackage allInstances collect: [:p| p name]) sort

Affichons le tout dans un Transcript. Ouvrez-le via le menu World > Tools > Transcript.



Puis évaluez le code suivant dans le Workspace:

|packageNames|
packageNames := MCPackage allInstances collect: [:p| p name].
packageNames sort do:[:name| Transcript show:name; cr]





Les objects PackageInfo référencent toutes les classes d'un package. Chaque objet MCPackage est associé à un objet PackageInfo.

Prenons par exemple le premier package et affichons toutes les classes qu'il contient via l'accesseur classes:

MCPackage allInstances first packageInfo classes




Pour extraire toutes les classes d'un package donné, une petite sélection fait l'affaire. Le code suivant renvoie toutes les classes du package System-Tools:

|systemTools|
systemTools := (MCPackage allInstances select: [:p| p name = 'System-Tools']) first.
systemTools packageInfo classes.





On peut accéder au commentaire de chaque classe via l'accesseur comment. Affichons le commentaire de la classe SpaceTally dans un Transcript:

Transcript show:SpaceTally comment



De là à afficher toutes les classes d'un package avec leurs commentaires, il n'y a qu'un pas:

|systemTools|
systemTools := (MCPackage allInstances select: [:p| p name = 'System-Tools']) first.
systemTools packageInfo classes do:[:c|
    Transcript show: '***',c name; cr.
    Transcript show: c comment; cr; cr]






2. Application Seaside pour consulter les commentaires

Rajoutons maintenant un peu de bling-bling à tout ça. Un Transcript c'est bien, une page web c'est plus rigolo.

Créons le package SimpleWebDoc qui contiendra une application Seaside minimaliste permettant de parcourir les commentaires des classes des packages.

Quelques précisions pour commencer. J'utilise ici l'image Pharo 1.0 RC1 09.11.04 avec Seaside et Pier, téléchargeable sur le site de Pharo à l'heure où j'écris ces lignes.
Le navigateur de classes utilisé par défaut est Browser (pour des raisons de performances et stabilité), or je vais utiliser le Package Browser. Pour changer le navigateur par défaut, ouvrez le navigateur de classe et dans le menu de la fenêtre sélectionnez Choose new default Browser.



Ici sélectionnez O2PackageBrowserAdaptator.



Lancez un nouveau navigateur de classe, vous devriez obtenir le Package Browser.



Ajoutons maintenant le package SimpleWebDoc: click droit, create package.



Nouveau clic droit sur le package et choisissez various > add to smart groups pour le rajouter dans l'onglet groups du Package Browser.





Créer application Seaside nécessite d'hériter WAComponent et de l'enregistrer comme application. Voici la déclaration de la classe SWDocView:

WAComponent subclass: #SWDocView
 instanceVariableNames: ''
 classVariableNames: ''
 poolDictionaries: ''
 category: 'SimpleWebDoc'

Pour afficher un composant, Seaside lui envoie le messager renderContentOn: en lui passant un objet WARenderCanvas sur lequel dessiner notre page web (en utilisant des brosses/brush). Nous allons ici construire un formulaire affichant tous les packages:

renderContentOn: html
 html form: [
  html select 
   list: (MCPackage allInstances collect: [:p| p name])
 ].

Améliorons la lisibilité du code en créant allPackageNames qui retourne les noms de tous les packages, dans l'ordre alphabétique:

allPackageNames 
 ^ (MCPackage allInstances collect: [:p| p name]) sort

et ajustons renderContentOn: en conséquence:

renderContentOn: html
 html form: [
  html select 
   list: self allPackageNames;
 ].

Testons ce que donne ce premier code. Déclarez le composant SWDocView comme application en évaluant ce qui suit dans un Workspace:

SWDocView registerAsApplication: 'simple-web-doc'

Pointez ensuite votre navigateur Web à l'adresse: http://localhost:8080/seaside/simple-web-doc



Ajoutons un bouton pour valider le formulaire:

renderContentOn: html
 html form: [
  html select 
   list: self allPackageNames;
  html submitButton with:'show'
 ].

Maintenant la partie intéressante: lorsqu'on valide le formulaire, on affiche la documentation du package. Pour cela nous stockons le nom sélectionné dans la variable d'instance selectedPackage en utilisant le callback de la brosse WASelectTag:

renderContentOn: html
 html form: [
  html select 
   list: self allPackageNames;
   callback: [:value| selectedPackage := value].
  html submitButton with:'show'
 ].

Si selectedPackage est définit, affichons la documentation:

renderContentOn: html
 html form: [
  html select 
   list: self allPackageNames;
   callback: [:value| selectedPackage := value].
  html submitButton with:'show'
 ].
 
 selectedPackage ifNotNil: [
   self renderPackage:selectedPackage On:html.
 ]

La méthode SWDocView>>renderPackage:On: qui reprends l'exemple précédent:

renderPackage:packageName On:html
  |package|
  package := (MCPackage allInstances select: [:p| p name = packageName]) first.
  package packageInfo classes do: [:c| 
    html heading level:2; with: c name.
      html break.
      html text: c comment.
      html horizontalRule.
  ]

et voici le résultat:



Amenons un peu de lisibilté en écrivant une méthode qui retourne toutes les classes d'un package:

classesOfPackageNamed: packageName
 |package|
 package := (MCPackage allInstances select: [:p| p name = packageName]) first.
 ^ package packageInfo classes

d'où une mise à jour de renderPackage:On:

renderPackage:packageName On:html
 (self classesOfPackageNamed:packageName) do: [:c| 
  html heading level:2; with: c name.
  html break.
  html text: c comment.
  html horizontalRule.
  ]


Bon nombre de choses sont à réaliser pour avoir une navigation correcte. Pour aller plus loin, consultez Dynamic Web Development with Seaside.