2012-01-03 3 views
5

루비 프로젝트에 대해 "클래스없는 DSL"을 만드는 방법을 알아 내려고합니다. 단계 정의가 오이클 단계 정의 파일에 정의되어 있거나 경로가 Sinatra 응용 프로그램에 정의 된 것과 비슷합니다. 내가이 방법의 무리와 함께 글로벌 (Kernel) 네임 스페이스를 오염하는 나쁜 습관이다 가정Ruby에서 클래스리스 DSL을 만드는 방법은 무엇입니까?

#sample.rb 

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

:

예를 들어, 내 모든 DSL 함수가 호출되는 파일을 갖고 싶어 내 프로젝트와 관련이 있습니다. 그래서 when_string_matchescall_another_method 메서드는 내 라이브러리에 정의되어 있고 sample.rb 파일은 어떻게 든 내 DSL 메서드의 컨텍스트에서 평가됩니다.

업데이트 : 다음은 이러한 DSL 방법은 현재 정의하는 방법의 예

방법이 서브 클래스되고있는 클래스에 정의 된 DSL은 (I 사이 이러한 방법을 다시 사용하는 방법을 찾으려면 간단한 DSL과 클래스 인스턴스) : 내 프로그램의 초기화 동안 일부 지점에서 그런

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 

가, 내가 sample.rb 파일을 구문 분석하고 이러한 작업을 저장할 나중에 실행되는 :

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 

내가 지금까지 가지고있는 문제 (그리고 위에서 언급하지 않은 여러 가지 것들)는 블록이 파일에 정의되어있을 때 인스턴스 메서드를 사용할 수없는 경우입니다. 그것은 지금 다른 수업에 있습니다). 하지만 내가하고 싶은 것은 Parser 클래스의 eval'ing보다는 인스턴스를 만들고 eval'ing하는 것입니다. 그러나 나는 이것을 어떻게하는지 모른다.

나는 그것이 의미가 있기를 바랍니다. 도움, 경험 또는 조언을 주시면 감사하겠습니다.

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

:

답변

4

당신이하고 싶은대로하는 방법에 대한 답을 얻기가 약간 어렵습니다. 나는 당신이 책 Eloquent Ruby을 보길 권하고 싶습니다. 그곳에는 아마도 당신에게 가치있는 DSL을 다루는 몇 개의 챕터가 있기 때문입니다. 당신은 이러한 다른 라이브러리가하는 일에 대해 몇 가지 정보를 요구 했으므로 간략하게 개요를 제공하려고합니다.

시나

당신이시나 코드 sinatra/main.rb로 보면 당신이 코드의 메인 라인에 Sinatra::Delegator을 확장 것을 볼 수 있습니다. Delegator 꽤 재미있다 ..

는 그것은

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 

을 위임하고자하는 모든 방법을 설정하고 필요한 경우는 오버라이드 (override) 할 수 있도록 클래스 변수로 위임하는 클래스를 설정합니다 ..

self.target = Application 

그리고 위임 방법은 잘 당신이 respond_to?을 사용하여 이러한 메소드를 오버라이드 (override) 할 수 있습니다 또는 메소드가 정의되어 있지 않은 경우는 target 클래스 부른다 ..

,
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 

오이

오이는 treetop language library 이용한다. 이것은 DSL을 구현하기위한 강력하고 (복잡하지만, 배우기 쉬운) 도구입니다. DSL이 많이 성장할 것으로 예상되면이 '큰 총'을 사용하는 방법을 배우는 데 투자 할 수 있습니다. 여기에 설명하기에는 너무 힘듭니다.

HAML

당신은 HAML에 대해 물어 보지 않았지만, 그것은 나무 꼭대기를 사용하지 않는 즉 '수동'구현되는 또 다른 DSL,입니다. 기본적으로 (여기 총 지나친 단순화는) 내가 지금 파일을 전처리 및 스택에 명령을 미는 직접적 방법를 호출하는 데 사용 생각하지만,

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) 
    ... 

... HAML 파일을 읽고 각 라인 with a case statement를 처리 일종의. 예 :

the plain method는 참고 definition of the constants은 다음과 같습니다 .. Kernel``에 정의 된

# 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

그 중 일부는 내 머리 위로 조금씩 이어져 있기 때문에 거기에서 소화해야 할 부분이 많지만 여전히 유용합니다. 감사! – Andrew

2

그냥 어떤 "문자열"에 대해 그것을 테스트, 인수로 정규 표현식을 취 when_string_matches라는 메소드를 정의하면 그 블록에 무엇이든 name 통과, 조건부 수익률을 얘기하고있어 이것은 본질적으로 모든 루비 DSL이다 : 종종 블록을 허용하는 흥미로운 이름을 가진 메소드.

+1

을 .... – Reactormonk

+0

그런 다음 메서드 정의를 변경하여 주어진 블록을 상태 변수와 함께 저장하여 나중에 실행합니다. – meagar

+0

좋아, 나는 나의 상황을 더 잘 설명하기를 희망하는 많은 코드 샘플로 나의 질문을 업데이트했다. 문제는 파싱하고 파일을 평가하고 블록이 처음 정의 된 곳에서 사용할 수없는 인스턴스 메서드를 호출하는 것입니다. – Andrew

3

모듈을 사용하여 코드를 구성 할 수 있습니다. Module#include 메소드를 사용하여 Module 클래스에 DSL 메소드를 추가 할 수 있습니다. 다음은 RSpec이 수행하는 방법입니다. 마지막 두 라인은 당신이 찾고있는 라인입니다. DSL을 간단하게 유지하는 것에 대한 @meagar에 +1하십시오!

또한 @UncleGene이 지적했듯이 RSpec은 DSL 방법으로 커널을 오염시킵니다. 나는 그걸 어떻게 극복해야할지 모르겠다. describe 방법을 사용하는 다른 DSL이있는 경우 어떤 describe을 사용했는지 파악하기 어렵습니다.

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

이것은 매우 도움이됩니다, 감사합니다! – Andrew

+1

여기에 확장되지 않는다면 커널을 오염시키지 않겠습니까? 'rspec'을 요구하십시오; Kernel.methods.grep/describe/=> 설명. 그리고 나는 더 나은 오염 모듈 (AFAIU OP는 오염을 피하려고 노력했다)을 확신하지 못했습니다. – UncleGene

+0

@UncleGene 당신 말이 맞습니다. 이 지점을 추가하려면 답변을 수정 중입니다. – CubaLibre

관련 문제