#33 Making a Plugin
Nell’ultimo episodio abbiamo mostrato come modificare una colonna di tipo datetime con un text field, piuttosto che con una serie di menu a tendina. La data e l’ora immesse per un task mediante text field sono poi interpretate prima di essere salvate sul database.
Lo abbiamo fatto creando un attributo virtuale nel nostro modello Task
, chiamato due_at_string
. Questo attributo ha un metodo getter che mostra la data in un formato adatto al salvataggio su database e un metodo setter che interpreta la stringa per farla diventare una data:
def due_at_string due_at.to_s(:db) end def due_at_string=(due_at_str) self.due_at = Time.parse(due_at_str) rescue ArgumentError @due_at_invalid = true end
Questo approccio funziona se c’è solo un attributo che vogliamo modificare in questo modo, ma se ce ne sono diversi, allora avremmo rapidamente parecchie duplicazioni nel modello, poichè creeremmo tanti getter e setter quanti sono gli attributi virtuali.
Invece di fare così, creiamo un metodo di classe nel nostro modello, che chiamiamo stringify_time
. Questo metodo creerà dinamicamente i metodi getter e setter per qualunque attributo che gli passiamo. Poichè questo comportamente molto probabilmente ci tornerà utile anche in altre applicazioni, lo svilupperemo sottoforma di plugin.
Creare un Plugin
Per cominciare, generiamo un plugin vuoto chiamato stringify_time
. Per farlo, lanciamo:
script/generate plugin stringify_time
dalla cartella root della nostra applicazione. Quesato comando genererà una serie di file in una nuova cartella stringify_time
sotto la cartella /vendor/plugins
.
Guardiamo il file init.rb
per primo. Questo file viene caricato quando viene caricato il plugin, per cui qui metteremo il require
del file nella cartella lib dove svilupperemo le funzionalità del plugin.
require 'stringify_time'
Il file stringify_time.rb
è dove scriveremo il codice che genera il getter e il setter tipo i metodi due_at_string
che abbiamo usato in precedenza. Iniziamo a definire un module con il metodo stringify_time
:
module StringifyTime def stringify_time(*names) end end
Il metodo stringify_time
accetta una lista di nomi come parametro. Abbiamo usato l’asterisco per indicare che quel metodo può prendere un numero di argomenti, ciascuno dei quali sarà messo in un array denominato names
.
Il metodo ciclerà sugli elementi dell’array names
e creerà due metodi (getter e setter) per ogni nome. Ruby rende questo genere di metaprogrammazione semplice; tutto ciò che dobbiamo fare per creare dinamicamente un metodo in una classe è chiamare la define_method
. Il codice per creare i metodi getter è:
names.each do |name| define_method "#{name}_string" do read_attribute(name).to_s(:db) end end
Questo codice itera su ogni nome presente nell’array e usa la define_method
per generare dinamicamente un metodo il cui nome è il nome passato, con la desinenza _string
, per cui se passiamo due_at
come nome, avremo un nuovo metodo di istanza due_at_string
. La define_method
accetta un blocco, il cui codice definito all’interno diverrà il corpo del metodo generando. Il metodo due_at_string
che abbiamo così creato poco fa, prende il valore dell’attributo di modello due_at
e lo restituirà come una stringa formattata. Facciamo la stessa cosa qui, ma poichè il nostro attributo è dinamico, dobbiamo usare la read_attribute
per recuperare il valore (anzichè direttamente il nome dell’attributo).
Con il getter definito, possiamo ora scrivere anche il setter:
define_method "#{name}_string=" do |time_str| begin write_attribute(name, Time.parse(time_str)) rescue ArgumentError instance_variable_set("@#{name}_invalid", true) end end
Usiamo nuovamente la define_method
. Dal momento che stiamo creando un metodo setter, il nome del metodo finisce con un uguale e, poichè necessita di un parametro, lo definiamo come una variabile del blocco:
Il nostro metodo due_at_string=
interpreta la stringa passatagli e la converte ad un valore Time
, poi imposta l’attributo due_at
a quel valore. Se il valore non può essere interpretato, l’eccezione è catturata e viene settato a true
un flag di istanza chiamato @due_at_invalid
. Nel nostro setter dinamico usiamo write_attribute
per impostare l’attributo dinamico e se quello fallisce, si chiama la instance_variable_set
per impostare la corrispondente variabile di istanza.
Mettendo insieme i vari pezzi, il nostro module StringifyTime
appare così:
module StringifyTime def stringify_time(*names) names.each do |name| define_method "#{name}_string" do read_attribute(name).to_s(:db) end define_method "#{name}_string=" do |time_str| begin write_attribute(name, Time.parse(time_str)) rescue ArgumentError instance_variable_set("@#{name}_invalid", true) end end end end end
C’è ancora un cambiamento che dobbiamo fare affinchè il nostro plugin funzioni. Dovremo chiamare stringify_time
in classi di modello che ereditano da ActiveRecord::Base
, per cui dovremo estendere ActiveRecord con il nostro nuovo module. Tornando al init.rb
possiamo fare ciò aggiungendo:
class ActiveRecord::Base extend StringifyTime end
Nota che stiamo usando extend
piuttosto che include
poichè ciò rende questo metodo nel nostro module un metodo di classe piuttosto che un metodo di istanza.
Ora che abbiamo definito il nostro plugin, lo possiamo usare nel nostro modello Task
, sostituendo i metodi getter e i setter con una chiamata a stringify_time
:
class Task < ActiveRecord::Base belongs_to :project stringify_time :due_at def validate errors.add(:due_at, "is invalid") if @due_at_invalid end end
Prima di aggiornare la pagina di modifica dei task per vedere se il nostro plugin funziona, dobbiamo riavviare il server web. Fatto ciò possiamo aggironare la pagina e vedere se funziona tutto.
Sembra tutto a posto. I campi temporali del task si vedono bene. Proviamo ora a immettere un valore non valido per vedere se viene gestito correttamente.
Anche questo funziona, ma la validazione sta funzionando solo perchè stiamo usando il nome sorretto della variabile di istanza dal plugin per riconoscere se il modello sia valido o meno.
def validate errors.add(:due_at, "is invalid") if @due_at_invalid end
Sembra piuttosto brutto fare affidamento ad una variabile di istanza generata da un plugin, per cui piuttosto usiamo un metodo.
def validate errors.add(:due_at, "is invalid") if due_at_invalid? end
Dobbiamo generare un altro metodo dinamico in stringify_time.rb
per creare questo metodo. Subito sotto al codice che crea i metodi getter e setter dinamici, possiamo aggiungere questo:
define_method "#{name}_is_invalid?" do return instance_variable_get("@#{name}_invalid") end
per creare il metodo _invalid?
.
E questo è quanto. Abbiamo creato con successo il nostro primo plugin Rails. Benchè sia piuttosto semplice, il principio è lo stesso a prescindere dalla complessità del plugin che si deve creare.