Skip to content

Instantly share code, notes, and snippets.

@jamesmartin
Last active August 29, 2015 14:18
Show Gist options
  • Select an option

  • Save jamesmartin/cbdb799a6b3c15cafb8b to your computer and use it in GitHub Desktop.

Select an option

Save jamesmartin/cbdb799a6b3c15cafb8b to your computer and use it in GitHub Desktop.
Define a struct-like object, with attributes that can be called normally, or yielded when they exist.
class SafeStruct
def self.with_attributes(params)
params.keys.map do |key|
define_method(key) do |&block|
ivar = instance_variable_get("@#{key}".to_sym)
if block && !ivar.nil?
block.call(ivar)
end
ivar
end
end
new(params)
end
def initialize(params)
params.each do |key, value|
instance_variable_set("@#{key}".to_sym, value)
end
end
def method_missing(symbol, *args, &block)
/without_(.*)/.match(symbol.to_s) do |matches|
ivar = matches[1]
yield if instance_variable_get("@#{ivar}".to_sym).nil?
return
end
/with_(.*)/.match(symbol.to_s) do |matches|
ivar = instance_variable_get("@#{matches[1]}".to_sym)
yield ivar unless ivar.nil?
return
end
super
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?('with_') || method_name.to_s.start_with?('without_') || super
end
end
require_relative 'safe_struct'
describe 'yieldable attributes' do
it 'yields an attribute value when the attribute is not nil' do
t = SafeStruct.with_attributes(name: 'some value')
expect { |b| t.name(&b) }.to yield_with_args('some value')
end
it 'does not yield when the attribute is not defined' do
t = SafeStruct.with_attributes(irrelevant: 'some value')
expect { |b| t.name(&b) }.not_to yield_control
end
it 'returns the value when a block is not given' do
t = SafeStruct.with_attributes(name: 'some value')
expect(t.name).to eq 'some value'
end
it 'returns nil when the attribute is defined but the value is nil' do
t = SafeStruct.with_attributes(name: nil)
expect(t.name).to eq nil
end
context "without attributes" do
it 'yields to the block when an attribute is not defined' do
t = SafeStruct.with_attributes(irrelevant: 'irrelevant')
expect { |b| t.without_name(&b) }.to yield_control
end
it 'does not yield when the attribute is defined and has a value' do
t = SafeStruct.with_attributes(name: 'some value')
expect { |b| t.without_name(&b) }.not_to yield_control
end
it 'responds to without_"attribute_name" when the attribute is defined' do
t = SafeStruct.with_attributes(name: 'some value')
expect(t).to respond_to(:without_name)
end
end
context "with attributes" do
it 'yields to the block when an attribute is defined' do
t = SafeStruct.with_attributes(name: 'some value')
expect { |b| t.with_name(&b) }.to yield_with_args('some value')
end
it 'does not yield when the value is not defined' do
t = SafeStruct.with_attributes(irrelevant: 'irrelevant')
expect { |b| t.with_name(&b) }.not_to yield_control
end
it 'responds to with_"attribute_name" when the attribute is defined' do
t = SafeStruct.with_attributes(name: 'some value')
expect(t).to respond_to(:with_name)
end
end
end
@jamesmartin
Copy link
Copy Markdown
Author

Could be useful in context where you want to be declarative, and not concerned with the notion of 'presence', like a Rails view, for example:

  - person = SafeStruct.with_attributes(name: 'Harry', email: nil)
  %p
    Welcome, #{person.name}.
  - person.email do |email|
    We will send email to you at: #{email}.

In this example, the email block would not be rendered, because the person object was initialized with an email attribute of nil.

The alternative is for the template to check for the presence of an attribute value before rendering:

  - person = Struct.new(:name, :email).new('Harry', nil)
  %p
    Welcome, #{person.name}.
  - if person.email.present?
    We will send email to you at: #{person.email}.

Which do you prefer? Why?

@jamesmartin
Copy link
Copy Markdown
Author

Good idea from @mattgay:

What if every attribute had in inverse yieldable?

- person.email do |email|
  We will send email to you at: #{email}.
- person.without_email do
  We don’t have an email address for you.

@jamesmartin
Copy link
Copy Markdown
Author

And now, with and without:

- person = SafeStruct.with_attributes(name: 'Harry', age: 50)

- person.with_email do |email|
  We will send email to you at: #{email}.

- person.without_email do
  We don't have an email address for you.

Almost looks like we're declaring behaviours based on types of person (with and without email addresses).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment