2012-01-03 19 views
5

Tôi đang cố gắng tìm ra cách tạo một loại "DSL ít lớp" cho dự án Ruby của tôi, tương tự như cách định nghĩa các bước được định nghĩa trong tệp định nghĩa bước dưa chuột hoặc các tuyến được định nghĩa trong ứng dụng Sinatra.Làm thế nào để tạo một DSL lớp ít trong Ruby?

Ví dụ, tôi muốn có một tập tin mà tất cả các chức năng DSL của tôi đang được gọi là:

#sample.rb 

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

tôi giả sử đó là một thực tế xấu để gây ô nhiễm (Kernel) namespace toàn cầu với một loạt các phương pháp đó là cụ thể cho dự án của tôi. Vì vậy, các phương pháp when_string_matchescall_another_method sẽ được xác định trong thư viện của tôi và tệp sample.rb bằng cách nào đó sẽ được đánh giá trong ngữ cảnh của các phương pháp DSL của tôi.

Cập nhật: Dưới đây là một ví dụ về cách các phương pháp DSL hiện đang định nghĩa:

DSL phương pháp được định nghĩa trong một lớp học đang được subclassed (Tôi muốn tìm một cách để tái sử dụng những phương pháp giữa DSL đơn giản và các trường hợp lớp):

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 

sau đó, tại một số điểm, trong quá trình khởi tạo các chương trình của tôi, tôi muốn phân tích các tập tin sample.rb và lưu trữ những hành động này được thực hiện sau:

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 

Vấn đề với những gì tôi có cho đến nay (và những thứ khác nhau mà tôi đã thử mà tôi không đề cập ở trên) là khi một khối được định nghĩa trong tệp, phương pháp thể hiện không khả dụng (tôi biết rằng nó đang ở trong một lớp khác ngay bây giờ). Nhưng những gì tôi muốn làm là giống như tạo ra một thể hiện và đánh giá trong bối cảnh đó hơn là đánh giá trong lớp Parser. Nhưng tôi không biết làm thế nào để làm điều này.

Tôi hy vọng điều đó có ý nghĩa. Bất kỳ trợ giúp, kinh nghiệm hoặc tư vấn nào sẽ được đánh giá cao.

Trả lời

4

Có một chút khó khăn để cung cấp cho bạn câu trả lời chính xác về cách làm những gì bạn đang yêu cầu. Tôi khuyên bạn nên xem cuốn sách Eloquent Ruby vì có một vài chương trong đó xử lý các DSL có thể có giá trị đối với bạn. Bạn đã yêu cầu một số thông tin về cách các thư viện khác làm những gì họ làm, vì vậy tôi có thể nhanh chóng cố gắng cung cấp cho bạn một cái nhìn tổng quan.

Sinatra

Nếu bạn nhìn vào mã Sinatra sinatra/main.rb bạn sẽ thấy rằng nó kéo dài Sinatra::Delegator vào dòng chính của mã. Delegator là khá thú vị ..

Nó thiết lập tất cả các phương pháp mà nó muốn ủy

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 

và thiết lập các lớp để ủy thác là một biến lớp để nó có thể được ghi đè nếu cần thiết ..

self.target = Application 

Và phương pháp đại biểu độc đáo cho phép bạn ghi đè lên các phương pháp này bằng cách sử dụng respond_to? hoặc nó gọi ra lớp target nếu phương pháp này không được định nghĩa ..

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 

Dưa chuột

dưa chuột sử dụng treetop language library. Đó là một công cụ mạnh mẽ (và phức tạp — tức là không tầm thường để học) để xây dựng các DSL. Nếu bạn dự đoán DSL của bạn phát triển rất nhiều thì bạn có thể muốn đầu tư vào việc học cách sử dụng 'khẩu súng lớn' này. Đó là quá nhiều để mô tả ở đây.

HAML

Bạn đã không hỏi về HAML, nhưng nó chỉ là một DSL được thực hiện 'bằng tay', tức là nó không sử dụng ngọn cây. Về cơ bản (tổng cộng đơn giản hóa ở đây) nó đọc file Haml và xử lý mỗi dòng 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) 
    ... 

Tôi nghĩ rằng nó được sử dụng để gọi ra phương pháp trực tiếp, nhưng bây giờ nó tiền xử lý các tập tin và thúc đẩy các lệnh vào một chồng của các loại. ví dụ. the plain method

FYI các definition of the constants trông như thế này ..

# 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ó rất nhiều thứ để tôi tiêu hóa ở đó vì một số trong số đó hơi hơn đầu tôi, nhưng nó vẫn hữu ích. Cảm ơn! – Andrew

2

Chỉ cần xác định một phương pháp gọi là when_string_matches mà phải mất một regex như một tham số, kiểm tra nó chống lại bất cứ điều gì "chuỗi" bạn đang nói về, và có điều kiện sản lượng, đi qua bất cứ điều gì name là khối của nó:

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

Về cơ bản, tất cả các DSL của Ruby là: Các phương thức với các tên thú vị thường chấp nhận các khối.

+1

... được xác định trên 'Kernel'. – Reactormonk

+0

Sau đó thay đổi định nghĩa phương thức của bạn để lưu trữ khối nó được đưa ra, cùng với bất kỳ biến trạng thái nào, để thực thi sau này. – meagar

+0

Ok, tôi đã cập nhật câu hỏi của mình với hàng tấn mẫu mã mà tôi hy vọng sẽ giải thích rõ hơn về tình huống của tôi. Vấn đề nằm ở việc phân tích cú pháp và đánh giá tệp và gọi các phương thức cá thể không có sẵn khi khối được định nghĩa lần đầu tiên. – Andrew

3

Bạn có thể sử dụng Mô-đun để sắp xếp mã của mình. Bạn có thể thêm các phương thức DSL của mình vào lớp Module bằng phương thức Module#include. Đây là cách RSpec thực hiện nó. Hai dòng cuối cùng là những gì bạn có thể đang tìm kiếm. +1 để @meagar về việc giữ cho DSL đơn giản!

Cũng như @UncleGene chỉ ra, RSpec làm hạt nhân gây ô nhiễm với phương pháp DSL. Tôi không chắc chắn làm thế nào để có được xung quanh đó. Nếu có một DSL khác với phương thức describe, sẽ rất khó để xác định xem describe phương thức nào đang sử dụng.

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

điều này rất hữu ích, cảm ơn bạn! – Andrew

+1

Không mở rộng ở đây gây ô nhiễm hạt nhân? yêu cầu 'rspec'; đặt Kernel.methods.grep/describe/=> mô tả. Và tôi không chắc rằng mô-đun gây ô nhiễm tốt hơn (AFAIU OP đang cố gắng tránh ô nhiễm) – UncleGene

+0

@ UncleGene bạn nói đúng. Tôi đang chỉnh sửa câu trả lời của mình để thêm điểm này. – CubaLibre

Các vấn đề liên quan