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
-
.base_converter(target) ⇒ Object
You’re not supposed to call this method.
-
.converter(target, options = {}) ⇒ Object
Builds a converter Proc that converts to target, with options.
-
.entries_converter(target, options = {}) ⇒ Object
Builds an enumerable converter Proc that converts to target, with options.
-
.human_input_converter(target) ⇒ Object
You’re not supposed to call this method.
-
.nil_on_failure_converter(target) ⇒ Object
You’re not supposed to call this method.
-
.stable_nil_converter(target) ⇒ Object
You’re not supposed to call this method.
-
.strong_type_checking_converter(target) ⇒ Object
You’re not supposed to call this method.
-
.weak_converter(target) ⇒ Object
You’re not supposed to call this method.
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, ={}) mode = [: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, ={}) converter(target, ).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 |