Module: Conversion

Defined in:
lib/conversion/core.rb,
lib/conversion/version.rb,
lib/conversion/accessors.rb,
lib/conversion/mode/weak.rb,
lib/conversion/mode/stable_nil.rb,
lib/conversion/mode/nil_on_failure.rb,
lib/conversion/mode/human_input/core.rb,
lib/conversion/mode/strong_type_checking.rb

Overview

Conversion Module

The Conversion Module provides a framework for:

  • converting values to others,

  • helping classes define attributes setters that convert their input.


Converting objects to others

Type casting

For instance, Integer conversion:

Conversion.converter(Integer).call(‘1’) # => 1 ‘1’.convert_to(Integer) # => 1 ‘1.4’.convert_to(Integer) # => 1 1.9.convert_to(Integer) # => 2 Conversion.entries_converter(Integer).call() # => [2, 3] :b=>.convert_entries_to(Integer) # => :b=>[2, 3]

Free conversion

Conversion is eventually based on Proc objects, so:

1.convert_to(proc { |x| x.succ }) # => 2 1.convert_to(:succ) # => 2

Class Attributes Management

If your class includes the Conversion::Accessors module, its attr_accessor and attr_writer methods now accept some options:

class Money include Conversion::Accessors attr_accessor :amount, :store_as => Float attr_accessor :currency, :store_as => Symbol def initialize(amount, currency=:euro) self.amount = amount self.currency = currency end def inspect() “#amount #currency” end end Money.new(‘1e2’, :dollar) # 100.0 dollar 100.convert_to(Money) # 100.0 euro

Conversion Modes

Conversion behavior can be altered with conversion modes. Module comes with five of them, called human_input, nil_on_failure, stable_nil, strong_type_checking, and weak, which we’ll define below.

Each of them alters the conversion process in a way that may be considered helpful.

Let’s start with a strong example, a class that fills itself from data that may come from a HTML form:

class MyFormInput include Conversion::Accessors

# alter the storage store_attributes_with_mode :human_input # define attributes attr_accessor :start_date, :end_date, :store_as => Date attr_accessor :amount, :store_as => Float # constructor def initialize(params) self.start_date = params self.end_date = params self.amount = params end end

Let’s give some user data to our class:

# - start_date is a m/d/y string # - end_date is a blank string # - amount contains an unbreakable space and a comma (thank you Excel) params = => ‘9/18/1973’, :end_date => ‘ ’, :amount=>‘ 1 234,5’ f = MyFormInput.new(params)

And let’s check the stored data:

f.start_date # => #<Date: …> f.end_date # => nil f.amount # => 1234.5

Ouééé !

The human_input mode is weak in the way that we’ll see below: when incoming data can’t be converted, it is stored as is. This mimics the ActiveRecord::Base behavior, which strictly separates data storage from data validation.

f.amount = “I’m richer that you” f.amount # => “I’m richer that you”

Let’s dig into our five modes now:

human_input

… was the real motivation behind the Conversion module:

‘ 1 234.5 ’.convert_to(Integer, :mode=>:human_input) # => 1235 ‘09/18/1973’.convert_to(Date, :mode=>:human_input) # => #<Date: …>

nil_on_failure

… silently discards any conversion error and returns nil instead.

‘abc’.convert_to(Integer, :mode=>:nil_on_failure) # => nil 123.convert_to(Bignum, :mode=>:nil_on_failure) # => nil 1234567890.convert_to(Fixnum, :mode=>:nil_on_failure) # => nil [‘1’,‘2’,‘a’].convert_entries_to(Integer, :mode=>:nil_on_failure) # => [1, 2, nil]

stable_nil

This mode makes sure nil is always converted to nil:

nil.convert_to(Integer) # => 0 nil.convert_to(Integer, :mode=>:stable_nil) # => nil nil.convert_to(:succ) # NoMethodError nil.convert_to(:succ, :mode=>:stable_nil) # => nil

strong_type_checking

… requires incoming data to already have the target type, except for nil:

‘1’.convert_to(Integer) # => 1 ‘1’.convert_to(Integer, :mode=>:strong_type_checking) # ArgumentError nil.convert_to(Integer, :mode=>:strong_type_checking) # => nil [1,‘2’].convert_entries_to(Integer, :mode=>:strong_type_checking) # ArgumentError

weak

… silently discards any conversion error and leaves data unchanged

‘abc’.convert_to(Integer, :mode=>:weak) # => ‘abc’ 1.convert_to(:upcase, :mode=>:weak) # => 1 [‘1’,‘2’,‘a’].convert_to(Integer, :mode=>:weak) # => [“1”, “2”, “a”] [‘1’,‘2’,‘a’].convert_entries_to(Integer, :mode=>:weak) # => [1, 2, “a”]

Hook up your own conversions

Conversion module leaves much room for your own conversions.

Read carefully the documentation for these methods :

  • Conversion.converter

  • Conversion.base_converter

You’ll understand how those methods are triggered:

  • Conversion.human_input_converter

  • Conversion.weak_converter

  • generally, Conversion.<mode>_converter

And when those are:

  • Integer.to_converter_proc

  • Date.to_human_input_converter_proc

  • generally, <conversion_target>.to_<mode>_converter_proc

After that, you’ll be able to write code like:

# human_input converts dates from 'm/d/y'.
# That's a pity there's no support for the French 'd/m/y' format.
# Let's add it by creating our own french_human_input conversion mode :

class Date
  class << self
    def to_french_human_input_converter_proc
      proc { |value|
        # clean and trim the value
        value = value.convert_to(Conversion::HumanInputString)
        if value.nil?
          nil
        else
          # "d/m/y" => date(y,m,d)
          elements = value.split(/[-\/]/).convert_entries_to(Integer)
          begin
            Date.civil(elements[2], elements[1], elements[0])
          rescue
            value
          end
        end
      }
    end
  end
end

'18/09/1973'.convert_to(Date, :mode=>french_human_input) => #<Date: ...>

Invariants

Whatever the object, it is true that:

object.convert_to(object.class).equal?(object)
# 1 -> Integer -> 1

For… many objects, it is true that:

object.convert_to(Conversion::HumanInputString).convert_to(object.class, :mode=>:human_input) == object
# 1 -> Conversion::HumanInputString -> '1' -> Integer -> 1

On that last invariant, see Conversion::HumanInputString.to_converter_proc

Defined Under Namespace

Modules: Accessors, EnumerableConverter, HumanInputString, VERSION

Class Method Summary collapse

Class Method Details

.base_converter(target) ⇒ Object

You’re not supposed to call this method. Use Conversion.converter or Conversion.entries_converter instead.

Returns a Proc that is the default converter to target.

  • If target is nil, returns nil

  • If target responds to :to_proc, returns target.to_proc

  • If target is a String, raise TypeError

  • If target is a Symbol, returns a Proc that send the target message to its parameter

  • If target is a Class, returns a Proc that creates a new object from its parameters



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/conversion/core.rb', line 262

def base_converter(target)
  converter = 
#        if target.respond_to?(:to_converter_proc)
#          target.to_converter_proc
#      
#        elsif target.nil?
    if target.nil?
      nil
  
    elsif target.respond_to?(:to_proc)
      target.to_proc
  
    elsif target.is_a?(String)
      raise TypeError.new("Strings can't be coerced into Proc")
  
    elsif target.is_a?(Symbol)
      proc { |value| value.send(target) }
    
    elsif target <= Integer
      proc do |value|
        if value.is_a?(Float)
          value.round
        else
          begin
            Integer(value)
          rescue Exception
            Float(value).round
          end
        end
      end
    
    elsif target == Float
      proc { |value| value.is_a?(Float) ? value : Float(value) }
    
    elsif target == NilClass
      proc { |value| nil }
  
    elsif target == String
      proc { |value| value.to_s }
  
    elsif target == Symbol
      proc { |value| value.to_sym }
  
    elsif target.is_a?(Class) &&
          target.respond_to?(:new) &&
          target.method(:new).arity != 0
      proc { |*value|
        if value.length == 1 && value.first.is_a?(target)
          value.first
        else
          target.new(*value)
        end
      }
    end

  converter || raise(ArgumentError.new("#{target.inspect} can't be coerced into Proc"))
end

.converter(target, options = {}) ⇒ Object

Builds a converter Proc that converts to target, with options.

Default converter is returned unless options contains a :mode (see Conversion.base_converter), and ArgumentError si raised if :mode is unsupported.

Precisely, if target has a “to_#mode_converter_proc” method, it is used (see Integer.to_converter_proc, Date.to_human_input_converter_proc).

Else, if Conversion has a “#mode_converter” method, it is used (see Conversion.weak_converter).

See also: Object#convert_to



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/conversion/core.rb', line 219

def converter(target, options={})
  mode = options[:mode]
  begin
    target_mode = (mode == :base) ? nil : mode
    target_builder = ['to', target_mode, 'converter_proc'].compact.join('_')
    return target.send(target_builder)
  rescue NoMethodError
    begin
      converter_mode = (mode.nil?) ? 'base' : mode.to_s
      converter_builder = converter_mode+'_converter'
      proc = send(converter_builder, target)
    rescue NoMethodError
      raise ArgumentError.new("#{options[:mode].inspect} is not a valid conversion mode")
    end
    
    begin
      target.singleton_class.instance_eval { define_method(target_builder) { proc } }
    rescue TypeError
      # target.singleton_class.instance_eval fails for some instances, like symbols
    end
    
    proc
  end
end

.entries_converter(target, options = {}) ⇒ Object

Builds an enumerable converter Proc that converts to target, with options.

Default converter is returned unless options contains a :mode (see Conversion.base_converter), and ArgumentError is raised if :mode is unsupported.

See also: Conversion.converter, Object#convert_entries_to



249
250
251
# File 'lib/conversion/core.rb', line 249

def entries_converter(target, options={})
  converter(target, options).convert_to(Conversion::EnumerableConverter)
end

.human_input_converter(target) ⇒ Object

You’re not supposed to call this method. Use Conversion.converter or Conversion.entries_converter instead.

This method implements the :human_input conversion mode.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/conversion/mode/human_input/core.rb', line 6

def human_input_converter(target)
  base_converter = converter(target, :mode=>:weak)

  return base_converter unless
    base_converter &&
    target.is_a?(Class)

  proc do |value|
    value = value.convert_to(Conversion::HumanInputString)
    if value.nil? || value.empty?
      nil
    else
      value = value.gsub(',','.').gsub(/ /,'') if value =~ /^[-+]?[ \d]*[\.,]?\d+$/
      base_converter.call(value)
    end
  end
end

.nil_on_failure_converter(target) ⇒ Object

You’re not supposed to call this method. Use Conversion.converter or Conversion.entries_converter instead.

This method implements the :nil_on_failure conversion mode.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/conversion/mode/nil_on_failure.rb', line 6

def nil_on_failure_converter(target)
  base_converter = converter(target)

  return nil unless base_converter
  
  if target.is_a?(Class)
    proc do |value|
      begin
        result = base_converter.call(value)
        result = nil unless result.is_a?(target)
      rescue Exception
        result = nil
      end
      result
    end
  else
    proc do |value|
      begin
        base_converter.call(value)
      rescue Exception
        nil
      end
    end
  end
end

.stable_nil_converter(target) ⇒ Object

You’re not supposed to call this method. Use Conversion.converter or Conversion.entries_converter instead.

This method implements the :stable_nil conversion mode.



6
7
8
9
10
11
12
13
14
15
16
# File 'lib/conversion/mode/stable_nil.rb', line 6

def stable_nil_converter(target)
  base_converter = converter(target)
  return nil unless base_converter
  proc do |value|
    if value.nil?
      nil
    else
      base_converter.call(value)
    end
  end
end

.strong_type_checking_converter(target) ⇒ Object

You’re not supposed to call this method. Use Conversion.converter or Conversion.entries_converter instead.

This method implements the :strong_type_checking conversion mode.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/conversion/mode/strong_type_checking.rb', line 6

def strong_type_checking_converter(target)
  base_converter = converter(target)

  return base_converter unless
    base_converter &&
    target.is_a?(Class)

  proc do |value|
    if value.nil?
      nil
    else
    raise ArgumentError.new("Invalid value for #{target}: #{value.inspect}") unless value.is_a?(target)
      value
   end
  end
end

.weak_converter(target) ⇒ Object

You’re not supposed to call this method. Use Conversion.converter or Conversion.entries_converter instead.

This method implements the :weak conversion mode.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/conversion/mode/weak.rb', line 6

def weak_converter(target)
  base_converter = converter(target)

  return nil unless base_converter
  
  if target.is_a?(Class)
    proc do |value|
      begin
        result = base_converter.call(value)
        result = value unless result.is_a?(target)
      rescue Exception
        result = value
      end
      result
    end
  else
    proc do |value|
      begin
        base_converter.call(value)
      rescue Exception
        value
      end
    end
  end
end