homeASCIIcasts

33: Fare un plugin 

(view original Railscast)

Other translations: En

Other formats:

Written by Andrea Salicetti

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.

Modificare il tempo in un textfield.

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.

I file creati in fase di generazione di un plugin.

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.

La pagina di edit ora funziona con il nostro plugin.

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 la validazione funziona.

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.