2012-01-03 7 views
5

Estoy tratando de descubrir cómo crear una especie de "DSL sin clase" para mi proyecto Ruby, similar a cómo se definen las etapas en un archivo de definición de paso de Cucumber o las rutas se definen en una aplicación Sinatra.¿Cómo crear un DSL sin clase en Ruby?

Por ejemplo, yo quiero tener un archivo donde están siendo llamados todos mis funciones DSL:

#sample.rb 

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

supongo que es una mala práctica de contaminar el (Kernel) espacio de nombres global con un montón de métodos que sean específico para mi proyecto. Entonces los métodos when_string_matches y call_another_method se definirían en mi biblioteca y el archivo sample.rb se evaluaría de alguna manera en el contexto de mis métodos DSL.

Actualización: Aquí está un ejemplo de cómo se definen actualmente estos métodos DSL:

El DSL métodos se definen en una clase que se está subclase (me gustaría encontrar una manera de volver a utilizar estos métodos entre la DSL simple y las instancias de clases):

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 

a continuación, en algún momento, durante la inicialización de mi programa, quiero analizar el archivo sample.rb y almacenar estas acciones a ejecutar más tarde:

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 

El problema con lo que tengo hasta ahora (y las varias cosas que he intentado que no mencioné anteriormente) es cuando se define un bloque en el archivo, el método de instancia no está disponible (sé que está en una clase diferente ahora mismo). Pero lo que quiero hacer es más como crear una instancia y evaluar en ese contexto en lugar de evaluar en la clase Parser. Pero no sé cómo hacer esto.

Espero que tenga sentido. Cualquier ayuda, experiencia o consejo sería apreciado.

Respuesta

4

Es un poco desafiante darle una respuesta fácil sobre cómo hacer lo que está pidiendo que haga. Te recomiendo que le eches un vistazo al libro Eloquent Ruby porque hay un par de capítulos sobre DSL que probablemente te resulten valiosos. Pidió información sobre cómo estas otras bibliotecas hacen lo que hacen, por lo que puedo tratar brevemente de darle una visión general.

Sinatra

Si nos fijamos en el código de Sinatra sinatra/main.rb verá que se extiende Sinatra::Delegator en la principal línea de código. Delegator es bastante interesante ..

En él se establecen todos los métodos que se desea delegar

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 

y establece la clase de delegar como una variable de clase para que pueda ser anulado si es necesario ..

self.target = Application 

Y el método delegado bien le permite reemplazar estos métodos utilizando respond_to? o hasta que llame a la clase target si no se define el método ..

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 

pepino

pepino utiliza el treetop language library. Es una herramienta poderosa (y compleja, es decir, no trivial de aprender) para construir DSL. Si prevé que su DSL crecerá mucho, entonces es posible que desee invertir en aprender a utilizar esta 'gran arma'. Es demasiado para describir aquí.

HAML

Usted no pidió sobre HAML, pero es sólo otro DSL que se implementó 'manualmente', es decir, que no utiliza copas de los árboles. Básicamente (simplificación excesiva aquí) lee el archivo haml y procesa cada línea 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) 
    ... 

Creo que es usado para llamar a métodos directamente, pero ahora es procesar previamente el archivo y empujando los comandos en una pila de tipo. p.ej. the plain method

FYI la definition of the constants se parece a esto ..

# 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

Hay mucho para mí para digerir allí ya que algo de eso está un poco sobre mi cabeza, pero aún así es útil. ¡Gracias! – Andrew

2

Basta con definir un método llamado when_string_matches que toma una expresión regular como un argumento, la contrasta con lo que sea "cadena" que está hablando, y de forma condicionada rendimientos, que pasa a lo name es a su bloque:

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

Esto es esencialmente todas las DSL de Ruby son: Métodos con nombres interesantes que a menudo aceptan bloques.

+1

... que se definen en 'kernel'. – Reactormonk

+0

A continuación, cambie la definición de su método para almacenar el bloque que se le proporciona, junto con las variables de estado, para su posterior ejecución. – meagar

+0

Ok, actualicé mi pregunta con un montón de ejemplos de código que espero que expliquen mejor mi situación. El problema radica en analizar y evaluar el archivo y los métodos de instancia de llamada que no están disponibles donde se definió por primera vez el bloque. – Andrew

3

Puede usar los Módulos para organizar su código. Puede agregar sus métodos DSL a la clase Module utilizando el método Module#include. Así es como lo hace RSpec. Las últimas dos líneas son las que probablemente estés buscando. ¡+1 a @meagar para mantener el DSL simple!

También como señala @UncleGene, RSpec contamina el Kernel con los métodos DSL. No estoy seguro de cómo solucionarlo. Si hubiera otra DSL con un método describe, sería difícil determinar qué describe estaba usando.

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

esto es muy útil, ¡gracias! – Andrew

+1

No se extiende aquí ¿contamina Kernel? requiere 'rspec'; pone Kernel.methods.grep/describe/=> describe. Y no estoy seguro de que el Módulo contaminante sea mejor (AFAIU OP intentaba evitar la contaminación) – UncleGene

+0

@UncleGene tienes razón. Estoy editando mi respuesta para agregar este punto. – CubaLibre

Cuestiones relacionadas