2012-01-03 9 views
5

Sto cercando di capire come creare una sorta di "DSL senza classi" per il mio progetto Ruby, in modo simile a come le definizioni dei passi sono definite in un file di definizioni passo Cucumber o le rotte sono definite in un'applicazione Sinatra.Come creare un DSL senza classi in Ruby?

Per esempio, io voglio avere un file in cui tutte le mie funzioni DSL sono chiamati:

#sample.rb 

when_string_matches /hello (.+)/ do |name| 
    call_another_method(name) 
end 

Penso che sia una cattiva pratica di inquinare la (Kernel) namespace globale con un gruppo di metodi che sono specifico per il mio progetto. Quindi i metodi when_string_matches e call_another_method verrebbero definiti nella mia libreria e il file sample.rb verrebbe in qualche modo valutato nel contesto dei miei metodi DSL.

Aggiornamento: Ecco un esempio di come questi metodi DSL sono attualmente definiti:

Il DSL metodi sono definiti in una classe che si sta sottoclassi (mi piacerebbe trovare un modo per riutilizzare questi metodi tra la semplice DSL e le istanze di classe):

module MyMod 
    class Action 
    def call_another_method(value) 
     puts value 
    end 

    def handle(text) 
     # a subclass would be expected to define 
     # this method (as an alternative to the 
     # simple DSL approach) 
    end 
    end 
end 

Poi ad un certo punto, durante l'inizializzazione del mio programma, voglio analizzare il file sample.rb e memorizzare queste azioni da eseguire in seguito:

module MyMod 
    class Parser 

    # parse the file, saving the blocks and regular expressions to call later 
    def parse_it 
     file_contents = File.read('sample.rb') 
     instance_eval file_contents 
    end 

    # doesnt seem like this belongs here, but it won't work if it's not 
    def self.when_string_matches(regex, &block) 
     MyMod.blocks_for_executing_later << { regex: regex, block: block } 
    end 
    end 
end 

# Later... 

module MyMod 
    class Runner 

    def run 
     string = 'hello Andrew' 
     MyMod.blocks_for_executing_later.each do |action| 
     if string =~ action[:regex] 
      args = action[:regex].match(string).captures 
      action[:block].call(args) 
     end 
     end 
    end 

    end 
end 

Il problema con quello che ho finora (e le varie cose che ho provato che non ho menzionato sopra) è quando un blocco è definito nel file, il metodo di istanza non è disponibile (lo so che è in una classe diversa in questo momento). Ma quello che voglio fare è più come creare un'istanza e valutare in quel contesto piuttosto che evalutare nella classe Parser. Ma non so come farlo.

Spero che abbia senso. Qualsiasi aiuto, esperienza o consiglio sarebbe apprezzato.

risposta

4

È un po 'difficile dare una risposta su come fare ciò che si sta chiedendo di fare. Ti consigliamo di dare un'occhiata al libro Eloquent Ruby perché ci sono un paio di capitoli che trattano di DSL e che probabilmente potrebbero essere utili per te. Hai chiesto alcune informazioni su come queste altre librerie fanno quello che fanno, quindi posso provare brevemente a darti una panoramica.

Sinatra

Se si guarda nel codice Sinatra sinatra/main.rb vedrai che si estende Sinatra::Delegator nella linea principale del codice. Delegator è abbastanza interessante ..

Esso definisce tutti i metodi che intende delegare

delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout, 
     :before, :after, :error, :not_found, :configure, :set, :mime_type, 
     :enable, :disable, :use, :development?, :test?, :production?, 
     :helpers, :settings 

e imposta la classe di delegare come una variabile di classe in modo che possa essere sostituito, se necessario ..

self.target = Application 

e il metodo delegato ben consente di sovrascrivere questi metodi utilizzando respond_to? oppure chiama alla classe target se il metodo non è definito ..

def self.delegate(*methods) 
    methods.each do |method_name| 
    define_method(method_name) do |*args, &block| 
     return super(*args, &block) if respond_to? method_name 
     Delegator.target.send(method_name, *args, &block) 
    end 
    private method_name 
    end 
end 

cetriolo

cetriolo utilizza il treetop language library. È uno strumento potente (e complesso, vale a dire non banale da apprendere) per la creazione di DSL. Se prevedi che la tua DSL cresca molto, potresti voler investire nell'imparare a usare questa "grande arma". È troppo da descrivere qui.

HAML

Lei non ha chiesto circa HAML, ma è solo un altro DSL che viene implementato 'manualmente', vale a dire che non usa le cime degli alberi. Fondamentalmente (grossolana semplificazione qui) legge il file Haml ed elabora ogni linea with a case statement ...

def process_line(text, index) 
    @index = index + 1 

    case text[0] 
    when DIV_CLASS; push div(text) 
    when DIV_ID 
    return push plain(text) if text[1] == ?{ 
    push div(text) 
    when ELEMENT; push tag(text) 
    when COMMENT; push comment(text[1..-1].strip) 
    ... 

Penso che sia usato per chiamare i metodi direttamente, ma ora è pre-elaborazione del file e spingendo i comandi in una pila di sorta. per esempio. the plain method

FYI il definition of the constants assomiglia a questo ..

# Designates an XHTML/XML element. 
ELEMENT   = ?% 
# Designates a `<div>` element with the given class. 
DIV_CLASS  = ?. 
# Designates a `<div>` element with the given id. 
DIV_ID   = ?# 
# Designates an XHTML/XML comment. 
COMMENT   = ?/ 
+0

C'è molto per me da digerire perché alcuni sono un po 'sopra la mia testa, ma è comunque utile. Grazie! – Andrew

2

Basta definire un metodo chiamato when_string_matches che prende una regex come argomento, test contro qualunque "stringa" di cui parli, e condizionatamente rendimenti, passando qualunque name è al suo blocco:

def when_string_matches(regex) 
    # do whatever is required to produce `my_string` and `name` 
    yield(name) if my_string =~ regex 
end 

Questo è essenzialmente tutto ciò che i DSL Ruby sono: metodi con nomi interessanti che accettano spesso blocchi.

+1

... che sono definiti in 'kernel'. – Reactormonk

+0

Quindi modificare la definizione del metodo per memorizzare il blocco fornito, insieme a qualsiasi variabile di stato, per l'esecuzione successiva. – meagar

+0

Ok, ho aggiornato la mia domanda con una tonnellata di esempi di codice che spero possano spiegare meglio la mia situazione. Il problema si trova con l'analisi e la valutazione del file e la chiamata di metodi di istanza che non sono disponibili in cui il blocco è stato definito per la prima volta. – Andrew

3

È possibile utilizzare i moduli per organizzare il codice. È possibile aggiungere i propri metodi DSL alla classe Module utilizzando il metodo Module#include. Ecco come lo fa RSpec. Le ultime due righe sono ciò che stai probabilmente cercando. +1 a @meagar su come mantenere semplice la DSL!

Anche come @UncleGene sottolinea, RSpec esegue il polling del kernel con i metodi DSL. Non sono sicuro di come aggirarlo. Se c'era un altro DSL con un metodo describe, sarebbe difficile determinare quale describe si stesse utilizzando.

module RSpec 
    module Core 
    # Adds the `describe` method to the top-level namespace. 
    module DSL 
     # Generates a subclass of {ExampleGroup} 
     # 
     # ## Examples: 
     # 
     #  describe "something" do 
     #  it "does something" do 
     #   # example code goes here 
     #  end 
     #  end 
     # 
     # @see ExampleGroup 
     # @see ExampleGroup.describe 
     def describe(*args, &example_group_block) 
     RSpec::Core::ExampleGroup.describe(*args, &example_group_block).register 
     end 
    end 
    end 
end 
extend RSpec::Core::DSL 
Module.send(:include, RSpec::Core::DSL) 
+0

questo è molto utile, grazie! – Andrew

+1

Non si estende qui inquinare il kernel? richiede 'rspec'; mette Kernel.methods.grep/describe/=> descrivi. E non sono sicuro che il modulo inquinante sia migliore (OP AFAIU stava cercando di evitare l'inquinamento) – UncleGene

+0

@UncleGene hai ragione. Sto modificando la mia risposta per aggiungere questo punto. – CubaLibre

Problemi correlati