benhamill / assembler

Block-based initializers for your Ruby objects.
https://github.com/benhamill/assembler#readme
MIT License
1 stars 0 forks source link

Think about type casting #5

Closed kerinin closed 10 years ago

kerinin commented 10 years ago

I'll sometimes do type casting on inputs to ensure they're in the shape I expect:

class Foo
  def initialize(options={})
    @number = options[:number].to_i
    @enumerable = Array(options[:enumerable])
  end
end

To get the same effect with assembler, we'd need to either remember to always do conversion when we construct instances or remember to always do the conversion inside the class when accessing the variables, neither of which is ideal.

Do you think we should work on a way to duplicate this functionality?

benhamill commented 10 years ago

Third way is to override the attr_readers that Assembler gives you with the casting in there:

def number
  @number.to_i
end

That's horrible, of course, because converting to Numerics might be OK, but to Strings or Arrays will cause it to create a bunch of objects all the time and... yuck.

This seems like a legit use case... but I'm not at all sure about the API. I love how simple the API currently is. If we want to maintain the same feature set we have and add this, we end up with something like...

class Foo
  extend Assembler
  assemble_with number: :to_i, enumerable: { convert: :to_ary, default: [] }
end

Or something abominable.

You got an API proposal?

kerinin commented 10 years ago

OK, after some thought, here's the best option I came up with. We provide two initializer functions, one for batch declaration (the one that's already implemented) and another one for declaring a single argument with options. Not sure what the names should be - I'll just use assemble_with_options for the sake of discussion:

class Foo
  extend Assembler

  # No change here
  assemble_with :required, optional: 'default'

  # This gives us flexibility going forward, because we can always add
  # more keys for additional metadata
  assemble_with_options :number, default: 'default', initializer: :to_i
  assemble_with_options :timestamp, initializer: ->(arg) { Time.at(arg) if arg.kind_of?(Integer) }

  # This might solve the lazy initialization problem too, if we passed self to
  # the initializer block
  assemble_with_options :host, initializer: ->(arg, foo) { arg || foo.redis.host } 
end

This lets us keep the existing API whose simplicity I also like, but declare more complex initialization logic on-demand as needed.

benhamill commented 10 years ago

See #11 for broader discussion about this.