Hallo Welt! Um vier Weisheitszähne erleichtert und mit Kühlakkus an den Hamsterbacken ist es mir nun eine kleine Freude, ein paar Worte zu den aktuellen Errungenschaften im Rahmen von Consolvix zu verlieren. Zuerst einmal muss ich feststellen, dass die "Kern-Infrastruktur" nicht so wirklich wachsen will. Außerdem habe ich noch kein schlüssiges Konzept für eine brauchbare GUI. Zwar kann man alle in der Datenbank vorhandene Entitäten wunderbar CRUDden, aber damit ist es ja nicht getan. Ich bin jedoch zuversichtlich, dass sich das innerhalb der nächsten Wochen klären lässt.
Was hat sich also ereignet? Ich habe mich im Rahmen einer akuten Notwendigkeit zu folgendem verleiten lassen: eine einfache Weboberfläche um Subversion-Repositories zu verwalten und ihnen ein Trac-Projekt zuzuordnen sowie letztere auch detailliert konfigurieren können, zu schreiben. Das an sich ist nichtt besonders anspruchvoll, nur Auslesen einiger Shellscript-Ausgaben (svnlook z.B.), lesen einiger verzeichnisinhalte und lesen und schreiben einer Konfigurations- (INI-) Datei. In meinem judendlichen Übereifer von den eleganten Fähigkeiten des allmächtigen ActiveRecord geblendet, sollten "natürlich" SvnRepositories und TracEnvironments als solche Objekte behandelt werden, damit die Konfiguartion auch schon einfach ist. OK, zugegebenermaßen wäre das bei SvnRepository nicht nötig gewesen, aber auf die Errungenschaften bei TracEnvironment bin ich schon ein wenig stolz ;-)...
OK, was zeichnet eine TracEnvironment aus?
- ist ein Verzeichnis, liegt zusammen mit anderen projekten in /var/trac => "Datenbank"
- hat eine INI-Datei zur Konfiguration => Werte = Spalten
- wird aufgesetzt durch den Aufruf von "trac-admin
initenv " => "save" - muss nachträglich über ein Webformular mit Validierung berarbeitet werden können => klassische ActiveRecord-bezogene Aufgabe
- beim Anlegen via "tracadmin initenv" wird _nur die Projektumgebung angelegt, nicht die Konfiguration. Also muss aus einer "sample"-Datei die konfiguration generiert werden, aber nur beim Anlegen => schöne Aufgabe für before_create
- bis auf die initial anzugebenden Argumente (s.o.) sind alle anderen Konfigurationsdirektiven optional und theoretisch auch beliebig erweiterbar => "Spalten" müssen irgendwie dymanisch generiert werden können.
Zuerst: hier kam wieder ActiveRecord::BaseWithoutTable zum Tragen. Alle bei der Initialisierung zwingend notwendigen Spalten wurden "fest verdrahtet", der Rest wird nachher dynamisch über die Konfigurationsdatei gemacht. Alo reicht zur Datendefinition folgendes:
class TracEnvironment < ActiveRecord::BaseWithoutTable
TRAC_BASE_PATH = '/var/trac'
TRAC_CONFIG_FILE = 'conf/trac.ini'
TRAC_TEMPLATE_PATH = '/usr/share/trac/templates'
column :name, :string
column :path, :string
column :trac__database, :string, 'sqlite:db/trac.db'
column :trac__repository_type, :string, 'svn'
column :trac__repository_dir, :string
column :trac__templates_dir, :string, TRAC_TEMPLATE_PATH
validates_presence_of :name, :trac__repository_dir
validates_inclusion_of :trac__repository_type,
:within => ['svn'],
:message => 'currently, only "svn" is supported.'
before_create :initenv
end
die Konstanten dürften selbsterklärend sein. Doch warum diekomischen Spaltennamen mit "__"? Bei denen handelt es sich um Werte, die nachher in die Konfigurationsdatei geschrieben werden. Das Problem mit der Konfigurationsdatei ist, dass diese in "Abschnitte" unterteilt ist, also z.B. sowas:
[abschnitt1]
var1 = val1
var2 = val2
[abschnitt2]
var3 = val3
...
das ist eine zweidimensionale Struktur, die ich über die Konvention ActiveRecord-Spaltenname = "#{Abschnittsname}__{Einstellunsname}" auf das @attributes-Array des ActiveRecord-Objektes abbilde. Unter Angabe der definierten Spalten kann man eine TracEnvironment erstellen, ganz genau wie man ein ganz normales AR-objekt in der Datenbank erstellt -- mit den gleichen Methoden, callbacks etc:
...
@trac_environment = TracEnvironment.new params[:trac_environment]
@trac_environment.save
...
Handelt es ich um ein neues Objekt (@newrecord==true), wird vor dem Speichern (übe beforecreate) erst die ganze Ordnerstruktur etc. angelegt, danach die Konfiguration geschrieben:
def update
temp_file_name = "#{self.config_file}~"
file = File.new(temp_file_name, 'w')
self.config_file_struct.each do |section, value|
file << "\n[#{section}]\n"
value.each do |attr, attr_val|
file << "#{attr} = #{self.send "#{section}__#{attr}"}\n"
end
end
FileUtils.move temp_file_name, self.config_file
@new_record = false
self
end
def create
command = "trac-admin #{self.path} initenv #{self.name} #{self.trac__database} #{self.trac__repository_type} #{self.trac__repository_dir} #{self.trac__templates_dir}"
@initialisation_log = "<em>#{command}</em>\n" + `#{command}`
case $?.exitstatus
when 0 # everything alright
return true
when 1 # general failure, just clean up and return
self.destroy
return false
when 2 # environment exists already
return false
else # unknown exit status (however, not a success): cleanup and issue an error
raise "trac-admin initenv returned exit code #{$?.exitstatus}"
self.destroy
end
self
end
ich spare mir jetzt eine detaillierte Erklärung, fragt mich wenn etwas unklar ist. Das Lesen der Konfiguration geschieht hier:
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
im Zusammenspiel mit einer überschriebenen methodmissing, die die Setter-"Methoden" für die in der Konfigurationsdatei vorhandenen Werte zu überschreiben. Das ist nötig, weil die eigentliche methodmissing eine spalte nicht anlegt, wenn diese nicht explizit definiert wurde (in diesem Fall über "column ...", ansonsten über auslesen der Datenbanktabelle). Die überschribene Methode fügt neue, unbekannte attribute einfach ins @attributes-Array ein, danach kann ActiveRecord damit arbeiten wie mit allen anderen Attributen.
def method_missing(name, *args)
method_name = name.to_s
if /^([a-z0-9_]+)__([a-z0-9_]+)=$/.match(method_name)
begin
super
rescue NoMethodError
key = "#{$1}__#{$2}"
@attributes[key] = args[0]
end
else
super
end
end
Alles in allem ist das Konzept, Dateisystemartige Strukturen mit Konfigurationswerten auf ActiveRecord abzubilden, in meinen Augen ziemlich praktisch. Auf Applikationsebene arbeitet man mit genau den gleichen Strukturen wie an anderer Stelle auch, und man spart sich jede Menge Ärger mit Validierung, Flusskontrolle über Callback-Methoden, usw. Nur ein Wermutstropfen bleibt noch: so schöne Aufrufe wie
TracEnvironment.find_by_trac__repository_type "svn"
gehen NOCH nicht. Vielleicht macht es Sinn, das später einmal zu implementieren, so viel Aufwand wäre es ja nicht!
Achja, und was wäre ein so langer Post ohne ein Bild:
Vielen Dank für's Lesen, Ich freue mich auf Kommentare!