About Ruby, Rails und 'drum und 'dran

This page contains an archive of all entries posted to /blog/wvk in the Ruby, Rails und 'drum und 'dran category. They are listed from oldest to newest.

Philosophisches und anderer Schmalz is the previous category.

Many more can be found on the main index page or by looking through the archives.

Powered by
Movable Type 3.31

Main

Ruby, Rails und 'drum und 'dran Archives

October 18, 2007

Von befreienden Einschränkungen

Mittlerweile wurde endlich ein klarer(er) Rahmen, in dem ich mich mit der Diplomarbeit befassen soll, festgelegt. Es bleibt sozusagen recht wenig vom ursprünglichen Umfang übrig, was aber nicht bedeutet, dass ich nun nichts mehr zu tun hätte :-). Die Arbeit wird sich nun auf die nachfolgenden Punkte beschränken. Grob gesagt wären das:
- Kernbereich (Framework, Modul-API; Modul: core)
- Benutzerverwaltung (Versch. Benutzertypen, Gruppen, Rollen, Rechte. Module: domain, email, auth)
- Prototypartige Implementierung eines optionalen Moduls (http)
- Installationsroutinen (sehr wahrscheinlich über Rake)

Viele Überlegungen zur Implementierung machen m.E. nur mit Sicht auf das Gesamtsystem Sinn, darum werde ich weiterhin beim Erarbeiten des Datenbankschemas weitestgehend alle beteiligten Entitäten mit einbeziehen, auch wenn diese in der Diplomarbeit nicht konkret behandelt werden.

Als nächstes sollte ich mir nun ein paar Gedanken zur Modulverwaltung machen, sprich wie diese in die Applikation integroert werden sollen. Ein Blogientrag zu dem Thema ist noch immer in Arbeit...

ActiveRecord für Fortgeschrittenere

Neulich habe ich im Buch "Agile Web Development with Rails" eine ganz nette Sache entdeckt, die mir für die Benutzer-Rechteverwaltung sehr gelegen kommt. Ich habe die Entitäten User, Group und AccessRight, wobei sowohl Benutzer als auch Gruppen beliebig viele Zugriffsrechte haben können (habtm). Üblicherweise würde man zur Erstellung der Rechte-Beziehungen einfache Join-Tables mit zwei Fremdschlüsseln verwenden (users_access_rights(user_id, access_right_id)). Da zusätzlich aber gespeichert werden soll, wer wann von wem welche Rechte zugewiesen bekommen hat, macht es Sinn, eine zusätzliche Entität UserAccessRight (dito für Gruppen) zu erstellen. Klassischerweise würde das dann so aussehen:


class User < ActiveRecord::Base
  belongs_to :user_access_right
end
# dito für Group...

class AccessRight < ActiveRecord::Base
  belongs_to :user
end

class UserAccessRight < ActiveRecord::Base
  has_one :user
  has_one :access_right
end

# Liste alle Rechte von Benutzer 1 auf:
user = User.find 1
user.user_access_rights.each do |acc|
  p acc.right.name
end

# darf der Benutzer also Benutzer hinzufügen?
if user.user_access_rights.find {|r| r.access_right_name == 'add_user'}
  p "Benutzer #{user.name} darf Benutzer hinzufügen."
end

Das funktioniert zwar wunderbar, aber bei genauerem hinsehen ist es bestenfalls "etwas unhübsch", um immer über die UserAccessRight-Entität zuzugreifen, wenn man eigentlich mit dem AccessRight selbst arbeiten möchte -- denn eigentlich soll ja weiterhin lediglich eine HABTM-Beziehung zwischen User und AccessRight bestehen, die aber so aufgelöst wird.

Dafür gibt's aber in Rails die folgende Lösung:


class User < ActiveRecord::Base
  has_many :user_access_rights
  has_many :access_right,
          :through => :user_access_right
end

class AccessRight < ActiveRecord::Base
  has_many :user_access_rights
  has_many :access_rights,
          :through => :user_access_rights,
          :unique => true
end

class UserAccessRight < ActiveRecord::Base
  belongs_to :user
  belongs_to :access_right
end

# Liste alle Rechte von Benutzer 1 auf:
user = User.find 1
user.rights.each do |acc|
  p acc.name
end

# darf der Benutzer also Benutzer hinzufügen?
if user.rights.find {|r| r.name == 'add_user'}
  p "Benutzer #{user.name} darf Benutzer hinzufügen."
end

So kann man also direkt, wie bei einer klassischen HABTM-Beziehung, auf die Rechte eines Benutzers zugreifen. Zusätzlich stehen aber auch unmittelbar die Meta-Informationen zu den einzelnen Rechten (wann und von wem verliehen) zur Verfügung.

Also ich finde das toll ;-)

Als Nächstes muss ich nur mal sehen, wie man halbwegs elegant an die Rechte herankommt, die einem Benutzer über seine Gruppenzugehörigkeiten verliehen werden. Zur Not tut's natürlich eine einfache Suchschleife über alle Gruppen und deren Rechte, oder die Rechte werden bei der Objektinitialisierung geladen, oder alles wird in einer nifty-tricky-magic Funktion gekapselt mit deren Innenleben sich lieber keiner auseinandersetzen sollte ;-)

November 12, 2007

ActiveRecord::BaseWithoutTable

Gestern brauchte ich (für ein Mailformular) so etwas wie ActiveRecord, aber ohne Tabelle. Wozu das, mag man fragen. Ganz einfach: ich brauchte einfache Validierung der eingebenen Formulardaten und demnächst auch Aggregation. Nach kurzem googlen nach "ActiveRecord without table" fand ich ActiveRecord::BaseWithoutTable. Dieses Plugin liefert einem alles, was ActiveRecord hat, nur eben ohne eine Tabelle für eine Entity aufbauen zu müssen. Toll, was? :)

Ich denke, dass dieses Plugin auch genau das ist, was ich bei meinen Apache-Configfiles einsetzen werde -- man erinnere sich -- auch da wollte ich "etwas wie ActiveRecord" einsetzen, mit Validierung, Aggregierung usw. Nun, wir kommen dem Ziel ein wenig näher...

Rails Helper v0.2

Soo, gerade ist Version 0.2 der Rails-helper-GUI fertig geworden. So schaut das Ding nun aus:

snap20.png

Folgendes hat sich geändert:

  • neues Tab mit RAKE-Buttons. Hier finden sich die meistverwendeten Rake-Aktionen db:migrate (optional mit Schemaversion), db:fixtures:load, db:schema:dump, db:test:prepare, log:clear, tmp:clear sowie test.
  • stdout und stderr werden in der eingebauten Konsole angezeigt
  • statt die Applikation vom Arbeitsverzeichnis aus aufrufen zu müssen, kann (muss) man nun das Arbeitsverzeichnis (Rails Root) explizit eingeben.
  • wenn nicht in das Rails Root-Verzeichnis gewechselt werden kann (z.B.weil Pfad ungültig/nicht gefunden), wird abgebrochen und eine Fehlermeldung erscheint.

Herunterladen kann man das Script hier: Download Rails Helper v0.2 Bitte daran denken, dass man zum Ausführen den KDE Kommander braucht (Aufrufen mit kmdr-executor rails-helper_0.2.kmdr).

Bugreports und Erweiterungswünsche sind stets Willkommen!

Nachtrag für Philipp ;-)

Ja, man kann die Buttons auch anders aussehen lassen, das ist ja das schöne an KDE ;-) schaustu, staunstu (über die Sinnhaftigkeit dieser Themes brauchen wir uns jetzt nicht streiten): snap21.png

snap22.png

Nachtrag:

Mittlerweile gibt Version 0.2.1 mit folgenden Fixes:

  • Tab-Reihenfolge korrigiert
  • Einblenden der Kommando-Felder bei "Scaffold" und "Controller" wiederhergestellt (muss irgendwie verloren gegangen sein bei v0.2

Der Download-Link (s.o.) ist immer noch der selbe.

December 9, 2007

Böser Bug in Rails 1.2.6

Das Serialisieren von Sitzungs-/Transaktionsobjekten zur Speicherung in der Datenbank mittels YAML ist in Rails eigentlich super-einfach:

class ConsolvixTransaction < ActiveRecord::Base
  serialize :data, Hash
  # ...

  def [](key)
    self.data[key]
  end

  def []=(key, value)
    self.data[key] = value
  end
end

Obiger Code veranlasst Rails, die data-Spalte vor dem Speichern zu serialisieren und nach dem laden wieder zu deserialisieren. Dann kann ich während eine Transaktion aktiv ist einfach über

@transaction[:address] = Address.new(params[:address])

z.B. ein neues Adressenobjekt in der Transaktion ablegen und theoretisch später wieder darauf zugreifen. Aber weit gefehlt, denn Rails deserialisiert das Objekt nachher nicht als Address-Objekt, sondern als YAMLObject.

Eine kurze Internetrecherche ergab, dass das tatsächlich ein Bug in Rails ist: http://dev.rubyonrails.org/ticket/7537 bzw. http://dev.rubyonrails.org/ticket/8933

Die bei Ticket Nr. 7537 vorgeschlagene Lösung, den folgenden Code in die environment.rb einzufügen, funktioniert zwar so weit, aber dass ich nun für jede theoretisch mögliche Entität, die einmal in einer Transaktion vorkommen könnte, ein require hinzufügen muss, ist mehr als nur hässlich.

require 'address'
require 'client'
require 'domain'
require 'email_address'
require 'system_user'
require 'user'
require 'email_account'
require 'ftp_account'
require 'hosting_account'

YAML.add_domain_type("ActiveRecord,2007", "") do |type, val|
  klass = type.split(':').last.constantize
  YAML.object_maker(klass, val)
end

class ActiveRecord::Base
  def to_yaml_type
    "!ActiveRecord,2007/#{self.class}"
  end
end

Kennt jemand anderes von euch dieses Problem auch? Existiert das noch in Rails 2.0 oder einer späteren Version als 1.2.6?

December 13, 2007

Rails Helper v0.3

Server Tab
Server starten/stoppen
Migration Fix
rake db:migrate VERSION=... geht nun
Stats Button
Button für Code-Statistik

Es hat sich wieder etwas getan um das numehr zum dritten Male hochgelobte Rails-Helper-Applikatiönchen, welches soeben in seiner 0.3. Version erschienen ist.

Erweiterungen

  • ein neues Tab zur Kontrolle des Webrick-Servers ist hinzugekommen. Zur Zeit kann man damit den Server nun nur starten oder stoppen. Eigetnlich soltle man das auch von Kdevelop aus bewerkstelligen können, aber das stoppend es Servers klappt dir nicht so wie vorgesehen -- daher diese eigene Implementierung.
  • im Tab "Rake" ist zum Leidwesen derer, die nichts von LOCs halten ( ;-)), ein weiterer Button zur Anzeige von Code-Statistiken (rake stats) hinzugekommen.
  • Die Terminalausgaben assen sich nun auf Knopfdruck löschen (noch nicht in den Screenshots abgebildet)

Bugfixes:

  • das Auswählen einer Migrations-Version != 0 funktioniert jetzt auch tatsächlich (hatte vorher einen Syntaxfehler wegen "+" vor der Versionsnummer gegeben) (Beweis siehe Screenie ;-))
  • die Schriftart in den Termninals wurde auf Monospace gesetzt, um die Ausgaben etwas Leserlicher zu gestalten.

Herunterladen kann man sich die neue Verson hier: Download Rails Helper v0.3. Zur Erinnerung, ausführen kann man das Script nur mit dem Kommander-Executor.

January 4, 2008

Auf zwei Schienen fährt man besser...

...so dachte ich mir, und stand vor der Entscheidung, ob ich nun den gleichen kapitalen Fehler begehen sollte, den ich schon während der Bachelorarbeit (nur damals mit PHP) begangen hatte: Sollte ich mitten während der Entwicklung einer Software das Framework aktualisieren? Bislang arbeitete ich ja mit Rails 1.2.6 und war gut zufrieden, doch dann wurden ein paar neue Features (wie eingebautes HTTP Basic Auth... Blog-Artikel dazu ist in Arbeit!) doch zu verlockend.... und ich tat es.

Ja, ich fahre nun auch auf Rails 2.0.

Und der Umstieg gelang erstaunlich reibungslos! Meine Schritte waren:

  • Lesen des Artikels mit den Änderungen in Rails 2.0 aus dem InnoQ-Wiki
  • Deinstallieren von Rails 1.2.6 (über APT, denn ich hatte Rails nicht über Gem installiert)
  • Installieren von Rails 2.0 (über Gem, denn Rails 2.0. gab's noch nicht in meinen Apt-Repos)
  • Deployen einer plain vanilla Rails App irgendwohin
  • Kopieren von /config/{boot, environment, routes, initializers}, /script/* und /public/{dispatch.*, *.html} von vanilla nach Consolvix
  • Korrigieren der Pfadangaben zu den includes in dispatch.fcgi, denn sonst spackte Apache
  • Umbenennen aller Views mit u.g. Script, das vielleicht auch jemand anderem irgendwann eine Hilfe sein könnte.

#!/usr/bin/env ruby
# rename all *.rhtml files within the /app/views directory ro *.html.erb
# Author: WvK <[email protected]>
require 'fileutils'
Dir.new('.').each do |entry|
  if File.directory? entry and not entry[0..0] == '.'
    Dir.new("./#{entry}").each do |file|
      if /.*.rhtml/.match file
        old_path = "./#{entry}/#{file}"
        new_path = old_path.gsub /.rhtml/, '.html.erb'
        if FileUtils.mv old_path, new_path
          p "mv #{old_path} -> #{new_path}"
        else
          p "ERROR RENAMING #{old_path} to #{new_path}!"
        end
      end
    end
  end
end

Tja, und was soll ich sagen: läuft! Bis auf die Sache mit der pagination, derer ich mich noch nicht angenommen habe, läuft alles weitestgehend tadellos. Ich bin froh, dass ich ab und an mal die Deprecation-Warnungen in /log/development.log zu Herzen genommen hatte!

Nun wo der Schritt also vollbracht ist, ist Consolvix bestimmt besser als je zuvor den Änderungen, welche die Zukunft bescheren wird, gewachsen ;-)

January 15, 2008

Neues System, neue Probleme

Mal wieder ging ein potentiell produktiver Tag ohne ein produktives Ergebnis zu Ende, weil mein Apache nicht mit den Rails-Anwendungen spielen wollte. mod_cgi meldete das übliche "premature end of script headers", was im Wesentlichen nichts weiter aussagt als "irgentwas stimmt da nicht". mod_fcgid hatte anschienend noch viel weniger Lust, denn eine Instanz nach dem Anderen hängte sich einfach nach einem "unexpected Signal 11" auf, später gab's dann auch noch lustige Segfaults dazu. Neuinstallieren von Apache und Modulen und trallala brachte alles nichts, auch die Schreib-, Lese- und Ausführrechte waren alle korrekt gesetzt FRUST.

Schließlich und endlich rang ich mich dann dazu durch, den Apachen mitsamt des Fcgid-Moduls selber zu kompilieren, denn vielleicht gab es ja tatsächlich Probleme, weil der eine oder andere Teil noch im 32 Bit-Modus lief. Glücklicherweise ist selber bauen und dann installieren unter Debian sehr sauber und einfach:

$ cd /usr/src
$ apt-get source apache2 libapache2-mod-fcgid
  ... [ratter] ...

das holt erstmal die ganzen Sourcen. Anschließend wird kompiliert:

$ cd apache2-2.2.6
$ fakeroot debian/rules binary
  ... [ratter ... ratter ... ratter] ...

dito für mod_fcgid. Im Verzeichnis /usr/src liegen anschließend einige .debs:

apache2_2.2.6-3_all.deb
apache2.2-common_2.2.6-3_amd64.deb
apache2-dbg_2.2.6-3_amd64.deb
apache2-doc_2.2.6-3_all.deb
apache2-mpm-event_2.2.6-3_amd64.deb
apache2-mpm-perchild_2.2.6-3_all.deb
apache2-mpm-prefork_2.2.6-3_amd64.deb
apache2-mpm-worker_2.2.6-3_amd64.deb
apache2-prefork-dev_2.2.6-3_amd64.deb
apache2-src_2.2.6-3_all.deb
apache2-threaded-dev_2.2.6-3_amd64.deb
apache2-utils_2.2.6-3_amd64.deb

Installiert werden sollten jedoch nicht alle (genauer gesagt geht das gar nicht). Es reicht, common, mpm-worker, utils und fcgid zu installieren.

# dpkg -i apache2.2-common_2.2.6-3_amd64.deb \
   apache2-mpm-prefork_2.2.6-3_amd64.deb \
   apache2-utils_2.2.6-3_amd64.deb

wenige Minuten später ... siehe da ... der Apache startet (OK, das hätte ich auch erwartet) und Rails rennt wieder. Geschafft!

Vielleicht noch ein paar Worte zu den verschiedenen MPM-Paketen oben: Seit der Version 2.X besteht der Apache aus zwei Teilen, einmal einem common-Paket und einem mpm-*-Paket. Die eigentliche Arbeit, nämlich das Bedienen der Clients, macht je ein "Arbeiter". Da der Apache bekanntlich durchaus 1000+ Anfragen pro Sekunde verarbeiten können muss, versteht sich von selbst, dass da irgendwie parallele Prozesse am Werk sein müssen. Und tatsächlich, "MPM" steht für Multi-Processing Module.

MPM-PREFORK ist das klassische Apache-Worker-Modell, wo sich einfach beim Start des Servers schon mehrere Apache-Instanzen im Speicher einnisten. Jede Instanz für sich genommen arbeitet singlethreaded. Dieses "Thread"-Modell ist sehr ausgereift und meines Wissens neben MPM-ITK das einzige, was mit Rails (und auch PHP5) zusammenarbeitet.

MPM-ITK erlaubt es, eine eigene UID und GID pro VirtualHost anzulegen, also ähnlich wie mod_suexec es für PHP tut. Das Klingt auf jeden Fall für meinen Server schonmal sehr interessant...

Die anderen MPM-Modelle sind entweder nicht sonderlich ausgereift oder sonstwie ungeeignet, deswegen werde ich nicht weiter drauf eingehen und mich einfach freuen, dass der alte Indianer wieder läuft :-)

INI-Files

Heute Nacht saß irgendwann wie betäubt vor meinem Quelltext und machte ein apathisches Auditing, bis ich auf die Stelle stieß, wo ich im TracEnviironment-Model die Trac.ini verarbeite. Wie bereits anderswo beschrieben, werden die Werte aus der ini-Datei als Attribute der jeweiligen TracEnvironmant-Instanz abgelegt, damit ich darauf zugreifan kann wie auf ein ActiveRecord-Objekt. Das übernimmt diese Methode:

def parse_config_file!(file=nil)
  config  = {}
  section = ''
  begin
    file = File.new(file||self.config_file)
    file.each_line do |line|
      if /^[([a-z0-9_]*)]$/.match line
        section = $1
      elsif /^\s*([a-zA-Z0-9_]*)\s*=\s*(.*)$/.match line
        self.send "#{section}__#{$1}=", $2
      end
    end
  rescue Errno::ENOENT
  end
  config
end

Zurückschreiben geht ähnlich, halt einfach den gleichen Weg rückwärts. Das funktioniert alles Prima so weit udn ich denke auch nicht, dass ich das so bald ändern werde. Dennoch packte mich die Neugier, ob es nicht vielleicht ein Rails-Plugin oder ein Gem zurm INI-Dateihandling gibt. Und siehe da: das inifile-Gem tut genau das: https://rubyforge.org/projects/inifile. Intern werden die "Sections" als Hash gespeichert und auf die Sections wird ebenfalls wie ein Hash zugegriffen.

Alles sehr nett. Viel Spaß damit, sollte jemand das mal brauchen...

January 20, 2008

Von etwas komplizierteren Beziehungen

Wenn A viele B hat, und C viele B hat, kann C auch viele A haben, wenn C :through benutzt. Wenn A aber viele B und B viele C hat, außerdem C zu vielen B gehört, dann kann C nicht auch viele A haben, selbst wenn B :through benutzt.

Ich wollt's nur mal erwähnt haben, sollte jemand von euch das auch mal probieren wollen.

Wie Bahnhof? Gut, dann nochmal kurz:

:through geht nicht bei hasandbelongstomany-Assoziationen.

Mein Problem:

class AccessRight
  # ...
end

class AccessRightGrant
  belongs_to :acces_right
  belongs_to :subject,
             :polymorphic => true
end

class SystemGroup
  has_many :access_right_grants,
           :as => :subject
  has_many :access_rights,
           :through => :access_right_grants
  has_and_belongs_to_many :users
end

class User
  has_and_belongs_to_many :system_groups
  has_many :access_right_grants,
           :as => :subject
  has_many :user_access_rights,
           :class_name => 'AccessRight'
           :through => :access_right_grants
  has_many :access_rights,
           :class_name => 'AccessRight',
           :through => system_groups
end

... geht also NICHT.

Was ich erreichen möchte, ist dies: User hat AccessRights, SystemGroup hat AccessRights, User hat alle AccessRights, die SystemGroup auch hat. User.access_rights soll also alle , nicht nur des User's AccessRights zurückliefern (in obigem Code sollten erstmal nur die AccessRights der Gruppen geladen werden, nicht alle). AccessRightGrant ist die Linking Table zwischen AccessRight und User/SystemGroup, wobei letztere über die polymorphische subject-Spalte gelinkt werden.

Dem mit o.g. Code generierten SQL nach zu urteilen, liegt das Problem bei der HABTM-Beziehung. Bestätigt hat das ein Tauchgang in den Rails-Source, der übrigens mit seinen n Metaprogrammier-Ebenen mehr als faszinierend und beeindruckend ist, wenn man sich mal etwas Zeit für ihn nimmt. Bei einer normalen has_many-Beziehung zwischen User und SystemGroupkönnte es funktionieren, ausprobiert habe ich das jedoch nicht.

Aber es lässt sich für alles eine Lösung finden und bis ich hierfür eine elegante Lösung gefunden habe, werde ich einfach ganz skrupellos brute-force-Methoden wie "lade alle Rechte und durchsuche das Array" benutzen. Tja, Rails, das haste nun davon :-)

January 21, 2008

Ein weiterer Rails-Bug

Vielleicht erinnert sich der eine oder andere aus der Schnittmenge der treuen Leser dieses Blogs und der regelmäßig bei unserer Diplomandenrunde in Dortmund Anwesenden noch daran, dass wir "damals" erwähnt hatten, dass es manchmal, aber nicht immer, in Rails zu einer Exception kommt, wenn man z.B. sowas hier probiert:

@client.user.system_groups.access_rights

obwohl alles ganz sicher korrekt mit has_many usw. definiert wurde. Ich werde hier nichts breittreten, was mich grob geschätzt die letzten 4 Stunden und viel mehr Nerven als diese Stunden Sekunden haben, gekostet hat, sondern es in einem einfachen Satz zusammenfassen:

NIEMALS in Join-Tables einen Primärschlüssel namens 'id' verwenden!!

das Problem ist, dass bei einem Join wie diesem:

User.find(17).system_groups

dieses SQL generiert wird:

SELECT * FROM `system_groups` 
  INNER JOIN groups_users 
    ON system_groups.id = groups_users.group_id 
WHERE (groups_users.user_id = 17 )

Na, wem fällt's auf? genau, da steht SELECT * FROM ..., und nicht, wie es sich gehört, SELECT system_groups.* .... Damit tritt der Spaltenname id nämlich zweimal im Resultat auf (einmal von der Gruppentabelle und einmal von der Join-Table), wobei letzten Endes die ID von der Join-Table von Rails verarbeitet wird und dann als FALSCHE system_group_id eingesetzt wird.

Nach dem Entfernen der id Spalte aus groups_users lief alles wie erwartet.

Man mag mich bei Gelegenheit dafür steinigen, dass ich in Join-Tables eine künstliche ID-Spalte verwende, aber Rails' Verhalten bezeichne ich in diesem Fall als schlichtweg falsch. I hereby declare it a bug.

January 25, 2008

Rubys Objektmodell

OK, was ich jetzt hier schreibe ist nichts neues und auch schon auf viel bessere Art und Weise von Why the Lucky Stiff beschrieben worden. Jedenfalls trieb mich ein Bug in Consolvix dazu, mich nochmals intensiver mit dem Thema Metaprogrammierung und dem Ruby-Objektmodell auseinanderzusetzen. Es folgen ein Paar Notizen.

  • Objekte in Ruby können nur Variablen aufnehmen -- Ein Ruby-Objekt hat keine Methoden
  • Objekte sind in erster Linie dies: Objekte. Erst in zweiter Linie sind sie Instanzen einer Klasse. Auch Klassen sind Objekte (ABER auf C-Quellcode-Ebene sind Object und Class zwei verschiedene Structs -- ich halte diese Information für wichtig, wenn man wirklich blicken will, warum diese Class-Objekt-Abhängigkeit keine unendliche Rekursion mit sich bringt)
  • Objekt-/Instanz-Methoden befinden sich in der Klasse, von der sich das Objekt ableitet. Eine Klasse ist nicht ein statisches, abstraktes Dingda, sondern ein Singleton-Objekt. Ein Objekt, das (Klassen-)Variablen UND Methoden aufnehmen kann. Eine Klasse eben. (Und hier würde dann die Rekursion einsetzen ;-))
  • Klassenmethoden befinden sich -- nein, nicht in der Klasse, die ja ein Objekt ist, sondern in der Klasse, die meistens Metaklasse genannt wird.
  • Klassen vererben ihre Methoden ihren Unterklassen. Somit steht, wenn man eine Klasse um eine neue Methode bereichert, sofort sämtlichen Unterklassen und sämtlichen Instanzen der Klasse diese Methode zur Verfügung. Wenn man aber einem einzelnen Objekt an dieser Stelle eine andere Implementierung dieser Methode geben möchte, so kann man dem Objekt diese Methode über die Metaklasse dieses Objektes zur Verfügung stellen. Methoden werden zuerst in der Metaklasse dann erst in der Klassenhierachie gesucht.
  • Das Konzept der Vererbung erweitert eine Klassenhierachie vertikal, während Module die Klassenhierachie horizontal erweitern -- man könne also sagen, dass das Konzept der Metaklassen die Hierachie in der tiefe erweitern -- aber das klingt gleich wieder so esotherisch...
  • Metaklassen können, da diese wiederum Objekte sind, selbst auch wieder Metaklassen enthalten. Eine Veränderung der Metaklasse einer Metaklasse einer Klasse zieht jedoch keinerlei Konsequenzen für die Klasse nach sich. Außerdem ist es sehr unwahrscheinlich, dass von diesem Konzept jemals jemand Gebrauch machen wird (OK, ich werde meinen kranken Geist darauf ansetzen, in einer freien Minute etwas passendes zu finden ;-))

Folgendes ER-Diagramm hab ich nach meinem Verständnis des Sachverhaltes zusammengestellt. Wenn jemand darin einen grundlegenden Fehler entdeckt, bitte sofort melden!

Dieses Klassendiagramm habe ich nach dem ASCII-Diagram aus einem (vermutlich bekannten) Post in der Ruby-Talk-Mailingliste gebastelt. Es stellt die Abhängigkeit der Klassen und Objekte untereinander dar. Wie man sieht, erbt letztendlich alles von Object. Die Klassen mit eingeklammerten Namen sind die jeweiligen Meta-Klassen (bzw. Singleton-Klassen-Instanzen...).

RubyObjectModelInheritances.png

Ich finde, dass das Bild etwas klarer wird, wenn man die Vererbung durch eine gerichtete Beziehung der Objekte untereinander ersetzt. super zeigt somit immer auf das Objekt der Elternklasse während self auf das Objekt der Singleton-Klasse zeigt.

RubyObjectModelReferences.png

So hat man im Wesentlichen die Abbildung des Ruby-Objektmodells auf C-Ebene vor sich: Vererbung und "magische Meta-Dingsda" sind nichts weiter als Structs, von denen einige lediglich Variablen, andere auch Methoden referenzieren können... der ganze Zauber gelüftet, aber eine Menge mehr Klarheit geschaffen :)

Zu dem ganzen ist anzumerken, dass nicht zu jedem Objekt a priori eine Singletonklasse existiert -- das würde unendlich viel Speicher erfordern. Eine Singletonklasse wird nur dann erstellt, wenn sie explizit angefordert wird, und zwar über

class Foo
  # ...
end

obj = Foo.new

class << obj
  # hier sind wir im Kontext der Metaklasse von obj!
end

class << Foo
  # hier sind wir im Kontext der Metaklasse von Foo!
end

class Foo
  class << self
    class << self
      # im Kontext der Metaklasse der Metaklasse von Foo ;-)
    end
  end
end

Alles klar? Nicht? Dann hilft nur lesen des o.g. Artikels von Why the Lucky Stiff oder sich einfach mit anderen Dingen beschäftigen...

January 27, 2008

Ein paar Worte zu Modulen in Ruby

Als Ergänzung zu meinem letzten Eintrag zum Thema "Rubys Objektmodell" folgen ein paar Notizen dazu, wie sich Module in das Gefüge einfügen (müssen sie ja irgendwie, sonst wäre es kein Gefüge ;-))

  • Wird ein Modul mittels include eingebunden, werden alle Modulmethoden zu Instanzmethoden der einbindenden Klasse.
  • Wird ein Modul mittels extend eingebunden, werden alle Modulmethoden zu Klassenmethoden der einbindenden Klasse.
  • es ist (in Rails) durchaus üblich, ein Modul Foo zu definieren, welches dann mit include Foo eingebunden wird, sowie ein Untermodul Foo::ClassMethods, welches mittels extend Foo::ClassMethods eingebunden wird. Diese Praxis habe ich einfach mal so übernommen.

So viel zu den Methoden. Doch was ist mit den Variablen? Wie wir alle wissen, werden Klassenvariablen weitervererbt, Instanzvariablen jedoch nicht (irgendwie logisch...). Aber bei Modulen...?

  • Da ein Modul nicht instanziiert werden kann, kann es auch keine Instanzvariablen (@foo) haben -- ganz einfach. Wird ein Modul mittels include eingebunden, müssen alle @foo's also Instanzvariablen der Objekte werden; wird es mit extend eingebunden, sind es eben Instanzvariablen der Klasse (und NICHT etwa Klassenvariablen oder so...)
  • Klassenvariablen (@@bar) werden bei include einfach übernommen -- sprich, was im Modul eine "Klassen"variable war, wird auch in (den Instanzen) der einbindenden Klasse als Klassenvariable erhalten bleiben. Bei extend jedoch sind alle im Modul definierten Klassenvariablen nur für die im Modul definierten Methoden erreichbar! Alle Versuche, aus dem Kontekt der Klasse (oder einer ihrer Instanzen) an sie heranzukommen, sind bei mir bislang gescheitert.

An einem Beispiel zeigt sich so was immer schön:

module Foo
  @@truth = 42

  def say_the_truth
    p @@truth
  end
end

class Bar
  include Foo
end
Bar.new.say_the_truth # 42 *SMILE*

class FooBar < Bar
  def say_something
    p @@truth
  end
end
FooBar.new.say_something # 42 *SMILE*

class Baz
  extend Foo

  def say_something
    p @@truth
  end
end
Baz.say_the_truth # 42 *SMILE*
Baz.new,say_something # PENG! WUMMS! "NameError: uninitialized class variable @@truth in Baz" ... *Autsch*

Alles klar?

October 4, 2008

Das leidige Thema 'Ruby und Strings'

Ruby kennt keine Strings.

Traurig, aber wahr. Ruby kennt nur irgendwie vermurkste Byte-Arrays, und deswegen musste ich leider heute bei einer formatierten Ausgabe wie dieser

User.find(:all).each do |user|
  if user.valid_system_user?
    puts "#{user.name.ljust(12, '.')} #{user.real_name[0..24].ljust(25, '.')} " +
         "#{user.uid.to_s.rjust(5)} #{user.gid.to_s.rjust(5)} " +
         "#{user.home_dir.ljust(23, '.')} #{File.exists?(user.home_dir).to_s.rjust(5)}"
  end
end

folgendes feststellen:

name         real name                   UID   GID home dir                exists?
wvk......... Xxxxxx xxx Xxxxxxx.......  1001  1001 /home/wvk..............  true
phl......... Xxxxxxx Xxxßxxxxxx......  1078   100 /home/phl.............. false
ua.......... Xxx Xxxxxxxxx............   510   100 /home/ua............... false
pg.......... Xxxxxxx Xxxxxx...........   505   505 /home/pg............... false
...

Na, wem fällt's auf? Genau, dem Kollegen mit dem "ß" im Namen fehlt ein '.'. Unschwer zu erraten, woran das liegt: rjust und ljust verwenden intern natürlich die length-Methode von Ruby, um die Länge des darzustellenden Strings zu ermitteln. Diese liefert aber nur bei den Standard-ASCII-Zeichen das richtige Ergebnis, bei anderen UTF8-Codierten Zeichen zwischen 1 und 3 zu viel:

~# irb
irb(main):001:0> "ß".length
=> 2
irb(main):002:0> "€".length
=> 3

Nanu. Um das Problem mit der falsch erkannten Stringlänge zu umgehen, könnte man folgendes verwenden:

$KCODE = 'u'
require 'jcode'

und dann, statt length, jlength verwenden. Dieser hässlichster aller Workarounds die mir diese Woche begegnet sind, funktioniert wie man es von jeder String-Funktion erwarten würde:

irb
irb(main):001:0> $KCODE = 'u'
=> "u"
irb(main):002:0> require 'jcode'
=> true
irb(main):003:0> 'äöü'.jlength
=> 3
irb(main):004:0> '€'.jlength
=> 1

Na bitte, wer sagt's denn. Aber was bringt das jetzt in Verbindung mit rjust? Nur so viel dass man beim selber neu Implementieren der Methoden eine zuverlässige Längenangebe hat (die Methoden jljust bzw. jrjust stellt jcode nämlich nicht zur Verfügung):

class String
  def jljust(len, char = ' ')
    if len > self.jlength
      self + char * (len - self.jlength)
    else
      self
    end
  end

  def jrjust(len, char = ' ')
    if len > self.jlength
      char * (len - self.jlength) + self
    else
      self
    end
  end
end

In verbindung mit dem neuen Code

User.find(:all).each do |user|
  if user.valid_system_user?
    puts "#{user.name.jljust(12, '.')} #{user.real_name[0..24].jljust(25, '.')} " +
         "#{user.uid.to_s.jrjust(5)} #{user.gid.to_s.jrjust(5)} " +
         "#{user.home_dir.jljust(23, '.')} #{File.exists?(user.home_dir).to_s.jrjust(5)}"
  end
end

sieht das Ergebnis dann auch so aus wie es aussehen sollte:

name         real name                   UID   GID home dir                exists?
wvk......... Xxxxxx xxx Xxxxxxx.......  1001  1001 /home/wvk..............  true
phl......... Xxxxxxx Xxxßxxxxxx.......  1078   100 /home/phl.............. false
ua.......... Xxx Xxxxxxxxx............   510   100 /home/ua............... false
pg.......... Xxxxxxx Xxxxxx...........   505   505 /home/pg............... false
...

Mehr zum Thema Unicode-Horror in Verbindung mit Rails gibt's unter http://wiki.rubyonrails.org/rails/pages/HowToUseUnicodeStrings zu lesen.

January 20, 2009

@shop.products.published.for_category(@category).since(3.years.ago).bought_by(@me)

Wenn es etwas gibt, was uns im aktuellen Projekt die Haut gerettet hat, dann ist es vermutlich named_scope. Gerade in dem Moment, wo Vladimir und ich eigentlich entschieden hatten, dass wie die Grenze des mit ActiveRecord elegant lösbaren überschritten hatten, sprang es mir ins Gesicht.

Hintergrund: Hat man ein komplexes Datenmodell mit vielen n-ären Abhängigkeiten und einer entsprechend kombinatorischen Anzahl an interessanten Abfragen, so stößt man mit find_by_x_and_y(:conditions => '...', :joins => '...') doch schnell an seine Grenze. Beispiel:

Category.all(
  :select     => 'categories.*, COUNT(DISTINCT categories_products.product_id) AS product_count,
                  AVG(product_reports.popularity) AS category_popularity',
  :joins      => 'INNER JOIN categories_products
                    ON categories_products.category_id=categories.id
                  INNER JOIN products
                    ON categories_products.product_id=products.id
                  INNER JOIN product_groups
                    ON products.group_id=product_group.id
                  INNER JOIN product_reports
                    ON product_reports.product_tribe_id=product_groups.id
                  INNER JOIN shop_products
                    ON shop_products.product_id=products.id',
  :group      => 'category_id',
  :conditions => 'shop_products.workflow_state=\'published\' AND categories.shop_id=42',
  :order      => 'category_popularity')

Wer findet heraus, was diese Query liefert?

Die Lösung für dieses Problem lautet, wie schon gesagt, Named Scopes. Diese erlauben es, Bedingungen bei Abfragen beliebig zu kombinieren. Ein Beispiel:

Category.for_shop(42).published.by_popularity

das liefert das gleiche Ergebnis wie die erste Query und ist gleichzeitig um längen flexibler. Was steckt dahinter? Dieses:

class Category
  named_scope :published,
    :conditions => "shop_products.workflow_state='published'"

  named_scope :by_popularity,
    :order  => 'AVG(product_reports.popularity)',
    :group  => 'category_id',
    :joins  => 'INNER JOIN categories_products
                  ON categories_products.category_id=categories.id
                INNER JOIN products
                  ON categories_products.product_id=products.id
                INNER JOIN product_groups
                  ON products.group_id=product_group.id
                INNER JOIN product_reports
                  ON product_reports.product_tribe_id=product_groups.id
                INNER JOIN shop_products
                  ON shop_products.product_id=products.id'

  named_scope :for_shop, lambda {|shop_id|
    {:conditions => ['categories.shop_id=?', shop_id]}
  }
end

Ein Named Scope ist also eine Art abgespeckter Beziehungsdefinition (has_many, has_one etc. definieren intern auch Scopes), die zur Generierung einer komplexeren SQL-Query herangezogen werden kann. Somit lässt sich die Komplexität der Queries wieder schön kapseln und außer an der Stelle ihrer Definition hat man nirgends in der Anwendung mit SQL zu tun, sondern mit gut lesbaren Konstrukten wie @shop.products.published.for_current_shop.since(3.years.ago).bought_by(@current_user). Außerdem spart man sich das händische Erstellen vieler spezialisierter find-Methoden!

Lang lebe ActiveRecord ;)

*) Anmerkung: Beim Projekt handelt es sich nicht, wie vllt angedeutet, um ein Shopsystem. Der Code wurde quasi anonymisiert.

February 17, 2009

Die Grenzen von Scopes in ActiveRecord

Nach anfänglicher Euphorie über die fantastischen Möglichkeiten von ActiveRecord's Scopes musste ich feststellen, dass diese leider kein Allheilmittel sind (ne, wirklich?).

Leider lassen sich in der Rails-Version 2.1.0 keine Scopes kombinieren, die alle eine :joins-Option definieren. Was sich als mehr als nur ein kleinwenig nervenaufreibend erweist, wenn man an diese Version gebunden ist.

[ein beispiel folgt vllt. später]

Glücklicherweise ändert(e) sich die Situation in Rails 2.2.2. Neben der Möglichkeit, ein default_scope für eine Modellklasse anzugeben (ähnlich wie es auch bei horherigen Versionen schon bei Assoziationen möglich war), werden auch verschachtelte Scopes mit mehreren :joins-Optionen korrekt verschmolzen. Deswegen habe ich heute ein paar Stunden damit verbracht, die ActiveRecord::Base#find-Implementierung aus 2.2.2 nach 2.1.0 rückzuportieren. Und siehe da: im Großen und Ganzen funktioniert es wie gewünscht. Dennoch kann es zu unerwarteten Ergebnissen kommen, wenn zwei verschiedene Join-Typen verwendet werden. So kann es vorkommen, dass die Reihenfolge der Scopes relevant ist, weil ihre Optionen von innen (ich rede von Verschachtelung...) nach außen hin ausgewertet wird. Also landen z.B. die Join-Anweisungen des innersten Scopes als erste in der SQL-Query und nicht als letztes. Das bedeutet bei LEFT OUTER JOINS vs. INNER JOINS Verwirrung, die hierher stammt:

class ActiveRecord::Base
  class << self

  def merge_joins(*joins)
    if joins.any?{|j| j.is_a?(String) || array_of_strings?(j) }
      joins = joins.collect do |join|
        # ...      ^ da müsste ein .reverse stehen!
      end
    # ...
    end
  end

In meinem heutigen Backport verwende ich also an der markierten Stelle ein joins.reverse.collect ..., was den Schmerz etwas lindert.

Um sicher zu gehen, dass die eigenen Scopes auch wirklich so funktionieren, wie sie sollen, ist es vermutlich das beste, nicht voreilig möglichst viele Optionen in find-Aufrufen wegzukapseln. Besser warten, bis die Applikation halbwegs ausgereift ist. Dann erst macht es Sinn (und das Leben leichter) alle Stellen mit find-Aufrufen über zwei Zeilen zu identifizieren und die Schnittmenge der dort auftretenden Argumente Scopes zusammenzufassen.

Was wohl leider bei komplexeren Datenmodellen unmöglich bleiben wird, ist, dass jeder Scope für sich genommen genauso sinnvollen Output liefert wie kombiniert mit anderen. Da hilft nur eines: Dokumentation!

March 19, 2009

Neue Dynamime-Version

Unter http://github.com/wvk/dynamime ist seit gerade die neue Version 0.9.1 des Dynamime-Railsplugins zu haben, die nun auch mit Rails 2.3 zusammenarbeitet.

Dynamime erweitert die in Rails verwendete Mime-Handling und Template Rendering-Infrstruktur um eine hierarchische Komponente. So können "Unter"-MIME-Typen definiert werden, die z.B. HTML auf speziellen (mobilen) Endgeräten abbilden. Damit ist es möglich, Views für verschiedene Geräte zu definieren, die jeweils (auch nur vereinzelt) spezielle Anforderungen an Inhalte oder Header stellen.

Dynamime verfügt über einen WURFL-Import Rake-Task, der aus WURFL-XML-Dateien Geräteklassen-Hierarchien importieren kann, die als Datengrundlage für die Browsererkennung herangezogen werden können.

Installiert wird Dynamime wie jedes Railsplugin:

ruby script/plugin install git://github.com/wvk/dynamime.git

Für nähere Informationen siehe README.rdoc.

Neue Dynamime-Version

Unter http://github.com/wvk/dynamime ist seit gerade die neue Version 0.9.1 des Dynamime-Railsplugins zu haben, die nun auch mit Rails 2.3 zusammenarbeitet.

Dynamime erweitert die in Rails verwendete Mime-Handling und Template Rendering-Infrstruktur um eine hierarchische Komponente. So können "Unter"-MIME-Typen definiert werden, die z.B. HTML auf speziellen (mobilen) Endgeräten abbilden. Damit ist es möglich, Views für verschiedene Geräte zu definieren, die jeweils (auch nur vereinzelt) spezielle Anforderungen an Inhalte oder Header stellen.

Dynamime verfügt über einen WURFL-Import Rake-Task, der aus WURFL-XML-Dateien Geräteklassen-Hierarchien importieren kann, die als Datengrundlage für die Browsererkennung herangezogen werden können.

Installiert wird Dynamime wie jedes Railsplugin:

ruby script/plugin install git://github.com/wvk/dynamime.git

Für nähere Informationen siehe README.rdoc.

March 31, 2009

Der Rails 2.3-Murks

Hier ein paar Notizen, die heute bei der Migration einer "enterprisey"-Rails-2.1-Anwendung auf Rails 2.3.2 entstanden sind. Vielleicht dienen sie dem einen oder anderen als kleine Hilfe:

  • ActionController::AbstractRequest doesn't exist anymore
  • relative_url_root has moved to ActionController::Base
  • Test Helpers must now ALL inherit from ActiveSupport::TestCase instead of e.g. Test::Unit::TestCase
  • instead of accessing/subclassing ActionController::AbstractRequest in tests etc, use ActionController::Request
  • String#truncate(str, len, omit) is now String#truncate(str, :length => len, :omission => omit)
  • ActionController::AbstractRequest.parse_query_parameters is now Rack::Utils.parse_query
  • to be able to use fixture_file_upload, post, get etc. in (functional) tests, ActionController::TestProcess has to be included in the test case
  • ActionController::TestCase::Assertions has to be included into ActiveSupport::TestCase to enable e.g. assert_redirected_to
  • request.headers['type'] is now empty in tests -- use request.headers['Content-Type'] instead.

Diese Liste wird sicherlich noch erweitert werden, wir sind ja erst am Anfang der Migration...

April 27, 2009

Rails I18n heiß diskutiert

Die halbherzig implementierte I18n-API des Rails 2.3-Frameworks bedarf dringend einer grundlegenden Überarbeitung, wenn Rails es einmal ernsthaft in Großunternehmen schaffen soll. Ich hoffe, dass am Ende die Vernunft siegen wird und Gettext es in den Rails-Kern schafft. Ein Versuch der Evangelisierung und eine angeregte Debatte zu dem Thema findet zur Zeit auf der Rails Core-Googlegroup statt. Kommentare und Einsichten sind willkommen!

May 7, 2009

Buttons oder Links?

vs. Delete

Wenn wir alle vorgeben, das Prinzip von REST verstanden zu haben, warum taucht dann immer wieder die Frage auf, warum (besonders zu Zeiten von Javascript, Ajax und Web 2.0) Links als Verweise auf Aktionen ("lösche Blogeintrag", "sende Formular ab"...) so schlimm seien.

Dabei ist es doch nur eine Hilfe für den Benutzer UND den Entwickler:

  • Links kann man (noch!) ohne Gefahr zu laufen, unerwünschte Nebeneffekte zu erzeugen, folgen. In REST-Sprech: Links verweisen auf Repräsentationen von Ressourcen.
  • Buttons verweisen auf Aktionen, die per definitionem (?) Nebeneffekte aufweisen. In REST-Sprech: Ihre Betätigung leitet Veränderungen an Ressourcen ein.

Will ich einem Benutzer also zeigen, was er sich alles ansehen kann, so biete ich ihm eine Liste von Links an. Will ich ihm hingegen die Möglichkeiten zur Veränderung der dargestellten Ressource(n) anbieten, so biete ich ihm eine Sammlung an Buttons.

Dieses Prinzip konsequent angewendet nimmt dem Entwickler m.E. sowohl im UI-Design als auch bei der Umsetzung einer RESTful Webanwendung (Denk-)Arbeit ab. Dem Benutzer der Anwendung macht es die Interpretation der UI ebenfalls erheblich leichter.

Was die Implementierung selber angeht, also ob Buttons nun Formulare abschicken oder ob die Aktionen via JavaScript eingeleitet werden, ist bei dieser Betrachtung erst einmal unerheblich. Ersteres ist der Accessibility von Webanweniungen natürlich bedeutend zuträglicher, zweiteres ist für "flashy" Web 2.0-Anwendungen wohl das Mittel der Wahl. Den Königsweg zu beschreiten und die Formulare per "unobtrusive Javascript" nach dem Laden der HTML-Seite zu verändern, um sie durch Ajax-getriebenes Verhalten zu ersetzen, ist aber auch kein bedeutender Mehraufwand (der sich aber auszahlt).

Leider wird o.g. Verhalten durch gewisse Standardmethoden in Ruby on Rails nicht unbedingt gefördert: link_to_remote setzt per default einen POST-Request ab, link_to_function existiert und button_to_remote existiert erst seit Rails 2.0 oder 2.1.

Sieht das jemand anders? Was sind dafür die Gründe? Diskussionsbeiträge sind willkommen!

Anmerkungen zu Michaels Kommentar (Danke dafür!): Ich gebe dier zu 100% recht, dass einen die richtige technische Lösung nicht davon entbindet, das Interface-Design derart zu gestalten, dass der Benutzer klar sieht wo er ohne Nachzudenken hinklicken darf und wo Vorsicht angebracht ist. Jedoch führt meines Erachtens die o.g. Regel, konsequent umgesetzt, automatisch zu besserem Interfacedesign: einfach nur deswegen, weil man eine einfache Regel umsetzt, an die sich der Benutzer gewöhnen kann. Siehe dazu auch die immer wieder geführte Diskussion, ob in einem Dialog der mit einem [OK] und einem [Abbrechen]-Button [OK] links oder rechts von [Abbrechen] angebracht werden sollte: es spielt keine Rolle, Hauptsache man wendet ein einziges Schema konsequent an.

Ich behaupte jedenfalls, dass es keinem Entwickler weh tut, konsequent Buttons für Aktionen und Links für "Ansichten" zu verwenden. Spätestens seit CSS 2.0 lassen sich beide ohnehin beliebig stylen. Und dann gibt's ja noch die Textbrowser... (nein, die sind beileibe nicht tot ;-))

May 26, 2009

Live von der RailsWayCon Berlin

Nach einem frühen aber entspannten Flug nach Berlin gibt's jetzt im Laufe des Tages immer mal das eine oder andere Bild von der RailsWayCon im BCC am Alexanderplatz. Natürlich ist die innoQ mit einigen Rails-Spezialisten vor Ort vertreten:

Raum und Zeit für Diskussionen mit den "üblichen Verdächtigen":

Einen sowohl abgedrehten als auch außerordentlich kompetenten Vortrag über Ola Binis neueste Programmiersprachenkreation namens "Ioke" gab's vom Meister persönlich:

Mehr gibt's später!

June 3, 2009

Ruby + Subversion

Wer sich wie ich dieses Wochenende im großen weiten WWW vergebens auf die Suche nach Dokumentation für die äußerst praktischen Ruby-Subversion-Bindings (libruby-svn) macht, dem sei an dieser Stelle gesagt: die Bibliothekt ist nicht dokumentiert.

Das Einzige, was mir weitergeholfen hat, sind folgende Blogs:

  • http://icepick.info/2008/09/25/using-rubys-svn-bindings/ (das einfachste beispiel)
  • http://www.oneofthewolves.com/2007/03/06/ruby-subversion-bindings-finally-some-documentation/ (schöne Einführung)
  • http://www.oneofthewolves.com/2007/12/22/ruby-subversion-bindings-better-documentation/ (Code und Unit Tests)

Das und ein Wenig Subversion-Anwenderwissen reicht bei Weitem aus, um z.B. Datenbankeinträge mit einem Subversion-Repository zu synchronisieren.

June 14, 2009

Ruby + Qt4

Ich habe heute wieder einmal ein Wenig mit Qt4 gespielt, diesmal jedoch nicht mehr mit C++, sondern Ruby. Die Voraussetzungen dazu beschränken sich auf die zu installierende Bibliothek libqt4-ruby und natürlich die Qt4 API-Referenz. Fazit: Sehr cool, die Eleganz von Ruby gepaart mit der mächtigen Qt-Bibliothek macht richtig Lust auf schöne Multiplatform-Desktop-Applikationen.

Die von QtRuby (libqt4-ruby) zur Verfügung gestellten Qt-Bindings orientieren sich zu sagen wir mal 98% an den Original-APIs, mit der Ausnahme dass das "Q" in denn C++-Klasssennamen durch ein "Qt::"-Prefix ersetz wurde. Was ich allerdings als weniger hübsch empfinde, ist die konsequente Weiterverwendung von camelCase auch bei Methodennamen -- es steht keine Rubyesquere under_score-Variante zur Verfügung.

Anders sieht das aus bei den Qt-/KDE-Ruby-Bindings in der Korundum-Bibliothek. korundum4 ist eine m.E. etwas weiter fortgeschrittene Qt-Schnittstelle für Ruby, die zusätzlich die umfangreichen KDE-Erweiterungen der Qt-Bibliotheken zur Verfügung stellt. Außerdem wird Camelcase und Underscore-Schreibweise bei allen Methodennamen gleichermaßen unterstützt und "Setter" werden wie gewohnt über "value = " (und zusätzlich über "setValue()") angeboten.

Während QtRuby konsequent auf Ruby-Datentypen setzt, verwendet Korundum (noch) eigene Wrapper-Datentypen an Stellen, wo die C++ API einen Pointer erwartet. So würde ein Aufruf von Qt::FontDialog.getFont(my_boolean, Qt::Font.new("Helvetica", 10), self) in Korundum so aussehen: Qt::FontDialog.getFont(Qt::Boolean.new, Qt::Font.new("Helvetica", 10), self). Dies soll sich laut Dokumentation aber in Zukunft ändern. Bis auf diese Dinge lassen sich Programme, die für QtRuby geschrieben wurden, auch mit Korundum weitestgehend ohne Änderungen auch mit Korundum betreiben.

Ein paar hilfreiche HowTos:

Happy Hacking!

January 27, 2010

JRuby Errno::ECONNREFUSED: Connection refused

Nun endlich war es so weit: ich versuchte es auch einmal mit JRuby. Beim Versuch, erste Gems zu installieren, bekam ich aber den hier:

quantino:~# jruby -S gem update --system
Updating RubyGems
ERROR:  http://gems.rubyforge.org/ does not appear to be a repository
ERROR:  While executing gem ... (Gem::RemoteFetcher::FetchError)
  Errno::ECONNREFUSED: Connection refused - Connection refused (http://gems.rubyforge.org/yaml)

Ich ließ es links liegen und wandte mich anderen Dingen zu. Später am selben Tag versuchte ich das WebOS-Docor-JAR zum Verarzten meines Palm Pre zu starten, welches erst einmal eine Verbindung ins Welt Weite Netz suchte -- Verbindung fehlgeschlagen.

Da musste es einen Zusammenhang geben! Und den gibt es: es sind nicht die Proxy-Einstellungen, wie lange vermutet, sondern seit einem nicht besonders lange zurück liegenden System-Upgrade (Debian unstable/experimental) muss folgender Parameter der JVM mit übergeben werden, sofern man sein System nicht für IPv6 eingerichtet hat:

-Djava.net.preferIPv4Stack=true

JRuby reicht Kommandozeilen-Parameter hinter -J an die JVM weiter, aus obigem Kommando wird demnach:

quantino:~# jruby -J-Djava.net.preferIPv4Stack=true -S gem update --system
Updating RubyGems
Nothing to update

Um die -J Option nicht ständig angeben zu müssen, tut es auch ein

export JAVA_OPTS="$JAVA_OPTS -Djava.net.preferIPv4Stack=true"

in ~/.bashc

Und damit läuft die Sache dann auch.