Skip to content

Instantly share code, notes, and snippets.

@nwjsmith
Created September 26, 2011 16:28
Show Gist options
  • Save nwjsmith/1242659 to your computer and use it in GitHub Desktop.
Save nwjsmith/1242659 to your computer and use it in GitHub Desktop.
module Formattable
def format(opts)
# ...
end
end
class Basketball::PlayerRecord
extend Formattable
format :fields =>[:rebounds_total, :minutes, :assists, :points], :with => '-'
end
@lsantos
Copy link

lsantos commented Sep 26, 2011

Okay, let's fill this in with some code so that we can refine it.

module Formattable  
def format(options)
    options.assert_valid_keys(:fields, :field, :with)
    fields = options[:fields] || options[:field]
    fields = [fields] if !fields.is_a?(Array)
    default_value = options[:with]

    fields.each do |field|
      self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
        def format_#{field}           
          value = read_attribute(:#{field})    
          value.present? ? value : '#{default_value}'
        end                                
      METHOD
    end
  end
end

class Basketball::PlayerRecord 
  format :field => :rebounds_total, :with => "-"
end

@nwjsmith
Copy link
Author

I think you can use Array#wrap to tighten this up a bit.

Also, we might want to write up some tests to understand how the class_eval in a module interacts with a class that is extended from it. I'm not sure what that behaviour is (will the class_eval be run in the context of the module or the class?)

module Formattable
  def format(options)
    options.assert_valid_keys(:fields, :field, :with)
    fields = Array.wrap(options[:fields] || options[:field])
    default_value = options[:with]
    fields.each do |field|
      class_eval <<-METHOD, __FILE__, __LINE__ + 1
        def format_#{field}
          value = read_attribute(:#{field})
          value.present? ? value : '#{default_value}'
        end
      METHOD
    end
  end
end

class Basketball::PlayerRecord
  extend Formattable
  format :field => :rebounds_total, :with => "-"
end

@nwjsmith
Copy link
Author

I just ran a test (see: https://gist.github.com/1242954) , and the above technique should work

@nwjsmith
Copy link
Author

Avoid name clashes:

module Formattable
  def format(options)
    options.assert_valid_keys(:fields, :field, :with)
    fields = Array.wrap(options[:fields] || options[:field])
    default_value = options[:with]
    fields.each do |field|
      class_eval <<-METHOD, __FILE__, __LINE__ + 1
        unless defined?(format_#{field})
          def format_#{field}
            value = read_attribute(:#{field})
            value.present? ? value : '#{default_value}'
          end
        end
      METHOD
    end
  end
end

class Basketball::PlayerRecord
  extend Formattable
  format :field => :rebounds_total, :with => "-"
end

@nwjsmith
Copy link
Author

Or raise an exception:

module Formattable
  def format(options)
    options.assert_valid_keys(:fields, :field, :with)
    fields = Array.wrap(options[:fields] || options[:field])
    default_value = options[:with]
    fields.each do |field|
      class_eval <<-METHOD, __FILE__, __LINE__ + 1
        unless defined?(format_#{field})
          def format_#{field}
            value = read_attribute(:#{field})
            value.present? ? value : '#{default_value}'
          end
        end
      METHOD
    end
  end
end

class Basketball::PlayerRecord
  extend Formattable
  format :field => :rebounds_total, :with => "-"
end

@lsantos
Copy link

lsantos commented Sep 26, 2011

How about this code?

module ActiverecordExtensions
  module Formattable
    def format(options)
      options.assert_valid_keys(:fields, :field, :with)
      fields = Array.wrap(options[:fields] || options[:field])
      default_value = options[:with]
      fields.each do |field|
        class_eval <<-METHOD, __FILE__, __LINE__ + 1
          unless defined?(format_#{field})
            def format_#{field}
              value = read_attribute(:#{field})
              value.present? ? value : '#{default_value}'
            end
          end
        METHOD
      end
    end
  end

  def self.included(base)
    base.extend         Formattable
  end
end

# some initializer file?
ActiveRecord::Base.include(ActiverecordExtensions)

@thuva
Copy link

thuva commented Sep 27, 2011

module SleazyFormatter
  class FormatterProxy < ActiveSupport::BasicObject
    def initialize(delegatee)
      @delegatee = delegatee
    end

    def method_missing(method_sym, *args)
      return super unless @delegatee.respond_to?(method_sym)

      default_value = args.first || "-"
      actual_value = @delegatee.send(method_sym)
      (actual_value.blank?) ? default_value : actual_value
    end

    def respond_to?(method_sym)
      @delegatee.respond_to?(method_sym) 
    end
  end

  def sleazy_formatter
    @sleazy_formatter ||= SleazyFormatter::FormatterProxy.new(self) 
  end
end

class Person
  include SleazyFormatter

  attr_reader :first_name, :last_name

  def initialize(first_name)
    @first_name = first_name
  end
end

p = Person.new("Thuva")
p.first_name # Thuva
p.last_name # nil
p.sleazy_formatter.first_name # Thuva
p.sleazy_formatter.last_name # -
p.sleazy_formatter.last_name("@") # @

@lsantos
Copy link

lsantos commented Sep 27, 2011 via email

@lsantos
Copy link

lsantos commented Sep 27, 2011

To illustrate my idea, here is some more code exploring the method_missing, following a method protocol.

Advantage:

  • We don't have to call sleazy_formatter first, so you save some typing

Drawback:

  • We will have to override the active record method missing
module Formattable
  SUFFIX_PROTOCOL_RE = /_with$/ 

  def method_missing(method_sym, *args)
    original_method = extract_original_method(method_sym)
    return super unless follow_protocol?(method_sym, original_method)

    default_value = args.first
    actual_value = send(original_method)
    actual_value.nil? ? default_value : actual_value
  end

  def extract_original_method(method_sym)
    method_sym.to_s.gsub(SUFFIX_PROTOCOL_RE, '')
  end

  def follow_protocol?(calling_method, original_method)
    SUFFIX_PROTOCOL_RE.match(calling_method.to_s) && respond_to?(original_method)
  end

end

class Person
  include Formattable

  attr_reader :first_name, :last_name

  def initialize(first_name)
    @first_name = first_name
  end

end

p = Person.new("Thuva")
p.first_name # Thuva
p.last_name # nil
p.first_name # Thuva
p.last_name_with('-') # -
p.last_name_with("@")

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