Ruby assignment methods won't receive a block?

I am building a DSL and have this module

module EDAApiBuilder
  module Client

    attr_accessor :api_client, :endpoint, :url

    def api_client(api_name)
      @apis ||= {}
      raise ArgumentError.new('API name already exists.') if @apis.has_key?(api_name)
      @api_client = api_name
      @apis[@api_client] = {}
      yield(self) if block_given?
    end

    def fetch_client(api_name)
      @apis[api_name]
    end

    def endpoint(endpoint_name)
      raise ArgumentError.new("Endpoint #{endpoint_name} already exists for #{@api_client} API client.") if fetch_client(@api_client).has_key?(endpoint_name)
      @endpoint = endpoint_name
      @apis[@api_client][@endpoint] = {}
      yield(self) if block_given?
    end

    def url=(endpoint_url) 
      fetch_client(@api_client)[@endpoint]['url'] = endpoint_url
    end

  end
end

so that I have tests like

  context 'errors' do

    it 'raises an ArgumentError when trying to create an already existent API client' do
      expect {
        obj = MixinTester.new
        obj.api_client('google')
        obj.api_client('google')
      }.to raise_error(ArgumentError,'API name already exists.')
    end

    it 'raises an ArgumentError when trying to create a repeated endpoint for the same API client' do
      expect {
        obj = MixinTester.new
        obj.api_client('google') do |apic|
          apic.endpoint('test1')
          apic.endpoint('test1')
        end
      }.to raise_error(ArgumentError,"Endpoint test1 already exists for google API client.")
    end 

  end

I would rather have #api_clientwritten as an assignment block

def api_client=(api_name)

so that I could write

obj = MixinTester.new
obj.api_client = 'google' do |apic|   # <=== Notice the difference here
  apic.endpoint('test1')
  apic.endpoint('test1')
end

because I think this notation (with assignment) is more meaningful. But then, when I run my tests this way I just get an error saying that the keyworkd_do is unexpected in this case.

It seems to me that the definition of an assignment block is syntactic sugar which won't contemplate blocks.

Is this correct? Does anyone have some information about this?

By the way: MixinTester is just a class for testing, defined in my spec/spec_helper.rb as

class MixinTester
  include EDAApiBuilder::Client
end

Upvotes: 2

Views: 50

Answers (1)

Eric Duminil
Eric Duminil

Reputation: 54223

SyntaxError

It seems to me that the definition of an assignment [method] is syntactic sugar which won't contemplate blocks.

It seems you're right. It looks like no method with = can accept a block, even with the normal method call and no syntactic sugar :

class MixinTester
  def name=(name,&block)
  end

  def set_name(name, &block)
  end
end

obj = MixinTester.new

obj.set_name('test') do |x|
  puts x
end

obj.name=('test') do |x| # <- syntax error, unexpected keyword_do, expecting end-of-input
  puts x
end

Alternative

Hash parameter

An alternative could be written with a Hash :

class MixinTester
  def api(params, &block)
    block.call(params)
  end
end

obj = MixinTester.new

obj.api client: 'google' do |apic|
  puts apic
end
#=> {:client=>"google"}

You could adjust the method name and hash parameters to taste.

Parameter with block

If the block belongs to the method parameter, and not the setter method, the syntax is accepted :

def google(&block)
  puts "Instantiate Google API"
  block.call("custom apic object")
end

class MixinTester
  attr_writer :api_client
end

obj = MixinTester.new

obj.api_client = google do |apic|
  puts apic
end

# =>
# Instantiate Google API
# custom apic object

It looks weird, but it's pretty close to what you wanted to achieve.

Upvotes: 2

Related Questions