conversion 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +0 -0
- data/README +3 -0
- data/Rakefile +85 -0
- data/lib/conversion/accessors.rb +137 -0
- data/lib/conversion/class_decoration.rb +107 -0
- data/lib/conversion/core.rb +336 -0
- data/lib/conversion/mode/human_input/class_decoration.rb +31 -0
- data/lib/conversion/mode/human_input/core.rb +74 -0
- data/lib/conversion/mode/human_input.rb +2 -0
- data/lib/conversion/mode/nil_on_failure.rb +33 -0
- data/lib/conversion/mode/stable_nil.rb +19 -0
- data/lib/conversion/mode/strong_type_checking.rb +24 -0
- data/lib/conversion/mode/weak.rb +33 -0
- data/lib/conversion/object_decoration.rb +23 -0
- data/lib/conversion/version.rb +9 -0
- data/lib/conversion.rb +22 -0
- data/lib/conversion_test.rb +336 -0
- data/test/conversion_test.rb +11 -0
- data/test/test_helper.rb +2 -0
- metadata +77 -0
data/CHANGELOG
ADDED
File without changes
|
data/README
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/contrib/rubyforgepublisher'
|
9
|
+
require 'fileutils'
|
10
|
+
include FileUtils
|
11
|
+
require File.join(File.dirname(__FILE__), 'lib', 'conversion', 'version')
|
12
|
+
|
13
|
+
AUTHOR = "Gwendal Roué"
|
14
|
+
EMAIL = "gr@pierlis.com"
|
15
|
+
DESCRIPTION = "Conversion module obviously allows object conversion and extends attr_writer/accessor"
|
16
|
+
RUBYFORGE_PROJECT = "conversion"
|
17
|
+
HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
|
18
|
+
BIN_FILES = %w( )
|
19
|
+
|
20
|
+
|
21
|
+
NAME = "conversion"
|
22
|
+
REV = File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
|
23
|
+
VERS = ENV['VERSION'] || (Conversion::VERSION::STRING + (REV ? ".#{REV}" : ""))
|
24
|
+
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
25
|
+
RDOC_OPTS = ['--quiet', '--title', "conversion documentation",
|
26
|
+
"--opname", "index.html",
|
27
|
+
"--line-numbers",
|
28
|
+
"--main", "README",
|
29
|
+
"--inline-source"]
|
30
|
+
|
31
|
+
desc "Packages up conversion gem."
|
32
|
+
task :default => [:test]
|
33
|
+
task :package => [:clean]
|
34
|
+
|
35
|
+
Rake::TestTask.new("test") { |t|
|
36
|
+
t.libs << "test"
|
37
|
+
t.pattern = "test/**/*_test.rb"
|
38
|
+
t.verbose = true
|
39
|
+
}
|
40
|
+
|
41
|
+
spec =
|
42
|
+
Gem::Specification.new do |s|
|
43
|
+
s.name = NAME
|
44
|
+
s.version = VERS
|
45
|
+
s.platform = Gem::Platform::RUBY
|
46
|
+
s.has_rdoc = true
|
47
|
+
s.extra_rdoc_files = ["README", "CHANGELOG"]
|
48
|
+
s.rdoc_options += RDOC_OPTS + ['--exclude', '^(examples|extras)/']
|
49
|
+
s.summary = DESCRIPTION
|
50
|
+
s.description = DESCRIPTION
|
51
|
+
s.author = AUTHOR
|
52
|
+
s.email = EMAIL
|
53
|
+
s.homepage = HOMEPATH
|
54
|
+
s.executables = BIN_FILES
|
55
|
+
s.rubyforge_project = RUBYFORGE_PROJECT
|
56
|
+
s.bindir = "bin"
|
57
|
+
s.require_path = "lib"
|
58
|
+
s.autorequire = "conversion"
|
59
|
+
|
60
|
+
#s.add_dependency('activesupport', '>=1.3.1')
|
61
|
+
#s.required_ruby_version = '>= 1.8.2'
|
62
|
+
|
63
|
+
s.files = %w(README CHANGELOG Rakefile) +
|
64
|
+
Dir.glob("{bin,doc,test,lib,templates,generator,extras,website,script}/**/*") +
|
65
|
+
Dir.glob("ext/**/*.{h,c,rb}") +
|
66
|
+
Dir.glob("examples/**/*.rb") +
|
67
|
+
Dir.glob("tools/*.rb")
|
68
|
+
|
69
|
+
# s.extensions = FileList["ext/**/extconf.rb"].to_a
|
70
|
+
end
|
71
|
+
|
72
|
+
Rake::GemPackageTask.new(spec) do |p|
|
73
|
+
p.need_tar = true
|
74
|
+
p.gem_spec = spec
|
75
|
+
end
|
76
|
+
|
77
|
+
task :install do
|
78
|
+
name = "#{NAME}-#{VERS}.gem"
|
79
|
+
sh %{rake package}
|
80
|
+
sh %{sudo gem install pkg/#{name}}
|
81
|
+
end
|
82
|
+
|
83
|
+
task :uninstall => [:clean] do
|
84
|
+
sh %{sudo gem uninstall #{NAME}}
|
85
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
unless Kernel.method_defined?(:singleton_class)
|
2
|
+
module Kernel
|
3
|
+
# Returns the singleton class of self
|
4
|
+
def singleton_class
|
5
|
+
class << self; self; end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module Conversion
|
11
|
+
# Classes that include Conversion::Accessors can manage the way they store their attributes.
|
12
|
+
#
|
13
|
+
# See Conversion::Accessors::ClassMethods
|
14
|
+
module Accessors
|
15
|
+
|
16
|
+
# Options for Class.attr_writer and Class.attr_accessor reserved by Conversion module.
|
17
|
+
ACCESSOR_OPTIONS = [:store_as, :store_mode]
|
18
|
+
|
19
|
+
# Append Conversion::Accessors features to base (the class that includes Conversion::Accessors)
|
20
|
+
def self.append_features(base) #:nodoc:
|
21
|
+
super
|
22
|
+
singleton_class.instance_eval do
|
23
|
+
alias_method :attr_accessor_before_conversion, :attr_accessor
|
24
|
+
alias_method :attr_writer_before_conversion, :attr_writer
|
25
|
+
end
|
26
|
+
base.extend(ClassMethods)
|
27
|
+
base.store_enumerable_attributes(false)
|
28
|
+
end
|
29
|
+
|
30
|
+
# This module is included in classes that includes Conversion::Accessors
|
31
|
+
module ClassMethods
|
32
|
+
|
33
|
+
@@attribute_conversion_mode = nil
|
34
|
+
|
35
|
+
# Defines readers and setters for each symbol argument.
|
36
|
+
#
|
37
|
+
# Option keys may contain :store_as and :store_mode, which are used as the target argument and :mode option of the Conversion.converter method.
|
38
|
+
#
|
39
|
+
# class A
|
40
|
+
# include Conversion::Accessors
|
41
|
+
# attr_accessor :attr1, :store_as => Integer
|
42
|
+
# attr_accessor :attr2, :store_as => Integer, :store_mode => :strong_type_checking
|
43
|
+
# end
|
44
|
+
# a = A.new
|
45
|
+
# a.attr1 = '1' # stores 1 in a.attr1
|
46
|
+
# a.attr2 = '1' # raises ArgumentError
|
47
|
+
|
48
|
+
def attr_accessor(*args)
|
49
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
50
|
+
accessor_options = ACCESSOR_OPTIONS.inject({}) { |h, accessor_option|
|
51
|
+
option_value = options[accessor_option]
|
52
|
+
h[accessor_option] = option_value if option_value
|
53
|
+
h
|
54
|
+
}
|
55
|
+
unless accessor_options.empty?
|
56
|
+
attr_reader(*args)
|
57
|
+
args.each { |symbol| attr_writer(symbol, accessor_options) }
|
58
|
+
else
|
59
|
+
attr_accessor_before_conversion(*args)
|
60
|
+
end
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# Defines setters for each symbol argument.
|
65
|
+
#
|
66
|
+
# Option keys may contain :store_as and :store_mode, which are used as the target argument and :mode option of the Conversion.converter method.
|
67
|
+
#
|
68
|
+
# class A
|
69
|
+
# include Conversion::Accessors
|
70
|
+
# attr_writer :attr1, :store_as => Integer
|
71
|
+
# attr_writer :attr2, :store_as => Integer, :store_mode => :strong_type_checking
|
72
|
+
# end
|
73
|
+
# a = A.new
|
74
|
+
# a.attr1 = '1' # stores 1 in a.attr1
|
75
|
+
# a.attr2 = '1' # raises ArgumentError
|
76
|
+
def attr_writer(*args)
|
77
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
78
|
+
if options
|
79
|
+
target = options[:store_as]
|
80
|
+
mode = options[:store_mode] || @@attribute_conversion_mode
|
81
|
+
converter = if @@convert_enumerable_attributes
|
82
|
+
Conversion.entries_converter(target, :mode=>mode)
|
83
|
+
else
|
84
|
+
Conversion.converter(target, :mode=>mode)
|
85
|
+
end
|
86
|
+
if converter
|
87
|
+
args.each { |symbol|
|
88
|
+
define_method("#{symbol}=") { |*value|
|
89
|
+
instance_variable_set("@#{symbol}", converter.call(*value))
|
90
|
+
}
|
91
|
+
}
|
92
|
+
return nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
attr_writer_before_conversion(*args)
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
# Specifies whether entries should be converted when an enumerable is stored in an attribute
|
100
|
+
#
|
101
|
+
# class A
|
102
|
+
# include Conversion::Accessors
|
103
|
+
# attr_accessor :attr1, :store_as => Integer
|
104
|
+
#
|
105
|
+
# store_enumerable_attributes
|
106
|
+
# attr_accessor :attr2, :store_as => Integer
|
107
|
+
#
|
108
|
+
# store_enumerable_attributes false
|
109
|
+
# attr_accessor :attr3, :store_as => Integer
|
110
|
+
# end
|
111
|
+
# a = A.new
|
112
|
+
# a.attr1 = ['1'] # raises
|
113
|
+
# a.attr2 = ['1'] # stores [1] in a.attr2
|
114
|
+
# a.attr3 = ['1'] # raises
|
115
|
+
def store_enumerable_attributes(bool=true)
|
116
|
+
@@convert_enumerable_attributes = bool
|
117
|
+
end
|
118
|
+
|
119
|
+
# Specifies the default storage mode for attributes defined after this method is called
|
120
|
+
#
|
121
|
+
# class A
|
122
|
+
# include Conversion::Accessors
|
123
|
+
# attr_accessor :attr1, :store_as => Integer
|
124
|
+
#
|
125
|
+
# store_attributes_with_mode :weak
|
126
|
+
# attr_accessor :attr2, :store_as => Integer
|
127
|
+
# end
|
128
|
+
# a = A.new
|
129
|
+
# a.attr1 = 'abc' # raises
|
130
|
+
# a.attr2 = 'abc' # stores 'abc' in a.attr2
|
131
|
+
def store_attributes_with_mode(mode)
|
132
|
+
@@attribute_conversion_mode = mode
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
class Bignum
|
2
|
+
class << self
|
3
|
+
# Returns a Proc that converts to Integer
|
4
|
+
#
|
5
|
+
# Bignum.to_converter_proc.call('1') # => 1
|
6
|
+
#
|
7
|
+
# Note that the result may not be a Bignum.
|
8
|
+
#
|
9
|
+
# See Integer.to_converter_proc, Conversion.converter, Object#convert_to
|
10
|
+
def to_converter_proc
|
11
|
+
Integer.to_converter_proc
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Fixnum
|
17
|
+
class << self
|
18
|
+
# Returns a Proc that converts to Integer
|
19
|
+
#
|
20
|
+
# Fixnum.to_converter_proc.call('1') # => 1
|
21
|
+
#
|
22
|
+
# Note that the result may not be a Fixnum.
|
23
|
+
#
|
24
|
+
# See Conversion.converter, Object#convert_to
|
25
|
+
def to_converter_proc
|
26
|
+
Integer.to_converter_proc
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Float
|
32
|
+
class << self
|
33
|
+
# Returns a Proc that converts to Float
|
34
|
+
#
|
35
|
+
# Float.to_converter_proc.call(1) # => 1.0
|
36
|
+
#
|
37
|
+
# See Conversion.converter, Object#convert_to
|
38
|
+
def to_converter_proc
|
39
|
+
proc { |value| value.is_a?(Float) ? value : Float(value) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Integer
|
45
|
+
class << self
|
46
|
+
# Returns a Proc that converts to Integer
|
47
|
+
#
|
48
|
+
# Integer.to_converter_proc.call(1) # => 1
|
49
|
+
# Integer.to_converter_proc.call(1.9) # => 2
|
50
|
+
# Integer.to_converter_proc.call('1.9') # => 2
|
51
|
+
# Integer.to_converter_proc.call('abc') # ArgumentError: invalid value for Float(): "abc"
|
52
|
+
#
|
53
|
+
# See Conversion.converter, Object#convert_to
|
54
|
+
def to_converter_proc
|
55
|
+
proc do |value|
|
56
|
+
if value.is_a?(Float)
|
57
|
+
value.round
|
58
|
+
else
|
59
|
+
begin
|
60
|
+
Integer(value)
|
61
|
+
rescue Exception
|
62
|
+
Float(value).round
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class NilClass
|
71
|
+
class << self
|
72
|
+
# returns a Proc that converts to nil
|
73
|
+
#
|
74
|
+
# NilClass.to_converter_proc.call(1) # => nil
|
75
|
+
#
|
76
|
+
# See Conversion.converter, Object#convert_to
|
77
|
+
def to_converter_proc
|
78
|
+
proc { |value| nil }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class String
|
84
|
+
class << self
|
85
|
+
# returns a Proc that converts to String
|
86
|
+
#
|
87
|
+
# String.to_converter_proc.call(1) # => '1'
|
88
|
+
#
|
89
|
+
# See Conversion.converter, Object#convert_to
|
90
|
+
def to_converter_proc
|
91
|
+
proc { |value| value.to_s }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class Symbol
|
97
|
+
class << self
|
98
|
+
# returns a Proc that converts to Symbol
|
99
|
+
#
|
100
|
+
# Symbol.to_converter_proc.call('abc') # => :abc
|
101
|
+
#
|
102
|
+
# See Conversion.converter, Object#convert_to
|
103
|
+
def to_converter_proc
|
104
|
+
proc { |value| value.to_sym }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,336 @@
|
|
1
|
+
# = Conversion Module
|
2
|
+
#
|
3
|
+
# Author:: Gwendal Roué (mailto:gr@pierlis.com)
|
4
|
+
# Copyright:: Copyright (c) 2006 Pierlis
|
5
|
+
# License:: Distributes under the same terms as Ruby
|
6
|
+
#
|
7
|
+
# -------
|
8
|
+
#
|
9
|
+
# The Conversion Module provides a framework for:
|
10
|
+
#
|
11
|
+
# - <b>converting values</b> to others,
|
12
|
+
# - helping classes define <b>attributes setters</b> that convert their input.
|
13
|
+
#
|
14
|
+
# -------
|
15
|
+
#
|
16
|
+
# == Converting objects to others
|
17
|
+
#
|
18
|
+
# === Type casting
|
19
|
+
#
|
20
|
+
# For instance, Integer conversion:
|
21
|
+
#
|
22
|
+
# Conversion.converter(Integer).call('1') # => 1
|
23
|
+
# '1'.convert_to(Integer) # => 1
|
24
|
+
# '1.4'.convert_to(Integer) # => 1
|
25
|
+
# 1.9.convert_to(Integer) # => 2
|
26
|
+
#
|
27
|
+
# Conversion.entries_converter(Integer).call(['2','3'])
|
28
|
+
# # => [2, 3]
|
29
|
+
# {:a=>'1', :b=>['2','3']}.convert_entries_to(Integer)
|
30
|
+
# # => {:a=>1, :b=>[2, 3]}
|
31
|
+
#
|
32
|
+
# === Free conversion
|
33
|
+
#
|
34
|
+
# Conversion is eventually based on Proc objects, so:
|
35
|
+
#
|
36
|
+
# 1.convert_to(proc { |x| x.succ }) # => 2
|
37
|
+
# 1.convert_to(:succ) # => 2
|
38
|
+
#
|
39
|
+
# == Class Attributes Management
|
40
|
+
#
|
41
|
+
# If your class includes the Conversion::Accessors module, its <tt>attr_accessor</tt> and <tt>attr_writer</tt> methods now accept some options:
|
42
|
+
#
|
43
|
+
# class Money
|
44
|
+
# include Conversion::Accessors
|
45
|
+
# attr_accessor :amount, :store_as => Float
|
46
|
+
# attr_accessor :currency, :store_as => Symbol
|
47
|
+
#
|
48
|
+
# def initialize(amount, currency=:euro)
|
49
|
+
# self.amount = amount
|
50
|
+
# self.currency = currency
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# def inspect() "#{amount} #{currency}" end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# Money.new('1e2', :dollar) # 100.0 dollar
|
57
|
+
# 100.convert_to(Money) # 100.0 euro
|
58
|
+
#
|
59
|
+
# == Conversion Modes
|
60
|
+
#
|
61
|
+
# Conversion behavior can be altered with <i>conversion modes</i>. Module comes with five of them, called <tt>human_input</tt>, <tt>nil_on_failure</tt>, <tt>stable_nil</tt>, <tt>strong_type_checking</tt>, and <tt>weak</tt>, which we'll define below.
|
62
|
+
#
|
63
|
+
# Each of them alters the conversion process in a way that may be considered helpful.
|
64
|
+
#
|
65
|
+
# Let's start with a strong example, a class that fills itself from data that may come from a HTML form:
|
66
|
+
#
|
67
|
+
# class MyFormInput
|
68
|
+
# include Conversion::Accessors
|
69
|
+
#
|
70
|
+
# # alter the storage
|
71
|
+
# store_attributes_with_mode :human_input
|
72
|
+
#
|
73
|
+
# # define attributes
|
74
|
+
# attr_accessor :start_date, :end_date, :store_as => Date
|
75
|
+
# attr_accessor :amount, :store_as => Float
|
76
|
+
#
|
77
|
+
# # constructor
|
78
|
+
# def initialize(params)
|
79
|
+
# self.start_date = params[:start_date]
|
80
|
+
# self.end_date = params[:end_date]
|
81
|
+
# self.amount = params[:amount]
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# Let's give some user data to our class:
|
86
|
+
#
|
87
|
+
# # - start_date is a m/d/y string
|
88
|
+
# # - end_date is a blank string
|
89
|
+
# # - amount contains an unbreakable space and a comma (thank you Excel)
|
90
|
+
#
|
91
|
+
# params = {:start_date => '9/18/1973', :end_date => ' ', :amount=>' 1 234,5'}
|
92
|
+
# f = MyFormInput.new(params)
|
93
|
+
#
|
94
|
+
# And let's check the stored data:
|
95
|
+
#
|
96
|
+
# f.start_date # => #<Date: ...>
|
97
|
+
# f.end_date # => nil
|
98
|
+
# f.amount # => 1234.5
|
99
|
+
#
|
100
|
+
# Ouééé !
|
101
|
+
#
|
102
|
+
# The <tt>human_input</tt> mode is <i>weak</i> 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.
|
103
|
+
#
|
104
|
+
# f.amount = "I'm richer that you"
|
105
|
+
# f.amount # => "I'm richer that you"
|
106
|
+
#
|
107
|
+
# Let's dig into our five modes now:
|
108
|
+
#
|
109
|
+
# === <tt>human_input</tt>
|
110
|
+
#
|
111
|
+
# ... was the real motivation behind the Conversion module:
|
112
|
+
#
|
113
|
+
# ' 1 234.5 '.convert_to(Integer, :mode=>:human_input) # => 1235
|
114
|
+
# '09/18/1973'.convert_to(Date, :mode=>:human_input) # => #<Date: ...>
|
115
|
+
#
|
116
|
+
# === <tt>nil_on_failure</tt>
|
117
|
+
#
|
118
|
+
# ... silently discards any conversion error and returns nil instead.
|
119
|
+
#
|
120
|
+
# 'abc'.convert_to(Integer, :mode=>:nil_on_failure) # => nil
|
121
|
+
# 123.convert_to(Bignum, :mode=>:nil_on_failure) # => nil
|
122
|
+
# 1234567890.convert_to(Fixnum, :mode=>:nil_on_failure) # => nil
|
123
|
+
# ['1','2','a'].convert_entries_to(Integer, :mode=>:nil_on_failure)
|
124
|
+
# # => [1, 2, nil]
|
125
|
+
#
|
126
|
+
# === <tt>stable_nil</tt>
|
127
|
+
#
|
128
|
+
# This mode makes sure nil is always converted to nil:
|
129
|
+
#
|
130
|
+
# nil.convert_to(Integer) # => 0
|
131
|
+
# nil.convert_to(Integer, :mode=>:stable_nil) # => nil
|
132
|
+
# nil.convert_to(:succ) # NoMethodError
|
133
|
+
# nil.convert_to(:succ, :mode=>:stable_nil) # => nil
|
134
|
+
#
|
135
|
+
# === <tt>strong_type_checking</tt>
|
136
|
+
#
|
137
|
+
# ... requires incoming data to already have the target type, except for nil:
|
138
|
+
#
|
139
|
+
# '1'.convert_to(Integer) # => 1
|
140
|
+
# '1'.convert_to(Integer, :mode=>:strong_type_checking) # ArgumentError
|
141
|
+
# nil.convert_to(Integer, :mode=>:strong_type_checking) # => nil
|
142
|
+
# [1,'2'].convert_entries_to(Integer, :mode=>:strong_type_checking)
|
143
|
+
# # ArgumentError
|
144
|
+
#
|
145
|
+
# === <tt>weak</tt>
|
146
|
+
#
|
147
|
+
# ... silently discards any conversion error and leaves data unchanged
|
148
|
+
#
|
149
|
+
# 'abc'.convert_to(Integer, :mode=>:weak) # => 'abc'
|
150
|
+
# 1.convert_to(:upcase, :mode=>:weak) # => 1
|
151
|
+
# ['1','2','a'].convert_to(Integer, :mode=>:weak) # => ["1", "2", "a"]
|
152
|
+
# ['1','2','a'].convert_entries_to(Integer, :mode=>:weak) # => [1, 2, "a"]
|
153
|
+
#
|
154
|
+
# == Hook up your own conversions
|
155
|
+
#
|
156
|
+
# Conversion module leaves much room for your own conversions.
|
157
|
+
#
|
158
|
+
# Read carefully the documentation for these methods :
|
159
|
+
# - Conversion.converter
|
160
|
+
# - Conversion.base_converter
|
161
|
+
#
|
162
|
+
# You'll understand how those methods are triggered:
|
163
|
+
# - Conversion.human_input_converter
|
164
|
+
# - Conversion.weak_converter
|
165
|
+
# - generally, Conversion.<mode>_converter
|
166
|
+
#
|
167
|
+
# And when those are:
|
168
|
+
# - Integer.to_converter_proc
|
169
|
+
# - Date.to_human_input_converter_proc
|
170
|
+
# - generally, <conversion_target>.to_<mode>_converter_proc
|
171
|
+
#
|
172
|
+
# After that, you'll be able to write code like:
|
173
|
+
#
|
174
|
+
# # human_input converts dates from 'm/d/y'.
|
175
|
+
# # That's a pity there's no support for the French 'd/m/y' format.
|
176
|
+
# # Let's add it by creating our own french_human_input conversion mode :
|
177
|
+
#
|
178
|
+
# class Date
|
179
|
+
# class << self
|
180
|
+
# def to_french_human_input_converter_proc
|
181
|
+
# proc { |value|
|
182
|
+
# # clean and trim the value
|
183
|
+
# value = value.convert_to(Conversion::HumanInputString)
|
184
|
+
# if value.nil?
|
185
|
+
# nil
|
186
|
+
# else
|
187
|
+
# # "d/m/y" => date(y,m,d)
|
188
|
+
# elements = value.split(/[-\/]/).convert_entries_to(Integer)
|
189
|
+
# begin
|
190
|
+
# Date.civil(elements[2], elements[1], elements[0])
|
191
|
+
# rescue
|
192
|
+
# value
|
193
|
+
# end
|
194
|
+
# end
|
195
|
+
# }
|
196
|
+
# end
|
197
|
+
# end
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# '18/09/1973'.convert_to(Date, :mode=>french_human_input) => #<Date: ...>
|
201
|
+
#
|
202
|
+
# == Invariants
|
203
|
+
#
|
204
|
+
# Whatever the object, it is true that:
|
205
|
+
# object.convert_to(object.class).equal?(object)
|
206
|
+
# # 1 -> Integer -> 1
|
207
|
+
#
|
208
|
+
# For... many objects, it is true that:
|
209
|
+
# object.convert_to(Conversion::HumanInputString).convert_to(object.class, :mode=>:human_input) == object
|
210
|
+
# # 1 -> Conversion::HumanInputString -> '1' -> Integer -> 1
|
211
|
+
#
|
212
|
+
# On that last invariant, see Conversion::HumanInputString.to_converter_proc
|
213
|
+
|
214
|
+
module Conversion
|
215
|
+
class << self
|
216
|
+
# Builds a converter Proc that converts to target, with options.
|
217
|
+
#
|
218
|
+
# Default converter is returned unless options contains a :mode (see Conversion.base_converter), and ArgumentError si raised if :mode is unsupported.
|
219
|
+
#
|
220
|
+
# Precisely, if target has a "to_#{mode}_converter_proc" method, it is used (see Integer.to_converter_proc, Date.to_human_input_converter_proc).
|
221
|
+
#
|
222
|
+
# Else, if Conversion has a "#{mode}_converter" method, it is used (see Conversion.weak_converter).
|
223
|
+
#
|
224
|
+
# See also: Object#convert_to
|
225
|
+
def converter(target, options={})
|
226
|
+
mode = options[:mode]
|
227
|
+
begin
|
228
|
+
target_mode = (mode == :base) ? nil : mode
|
229
|
+
target_builder = ['to', target_mode, 'converter_proc'].compact.join('_')
|
230
|
+
return target.send(target_builder)
|
231
|
+
rescue NoMethodError
|
232
|
+
begin
|
233
|
+
converter_mode = (mode.nil?) ? 'base' : mode.to_s
|
234
|
+
converter_builder = converter_mode+'_converter'
|
235
|
+
proc = send(converter_builder, target)
|
236
|
+
rescue NoMethodError
|
237
|
+
raise ArgumentError.new("#{options[:mode].inspect} is not a valid conversion mode")
|
238
|
+
end
|
239
|
+
|
240
|
+
begin
|
241
|
+
target.singleton_class.instance_eval { define_method(target_builder) { proc } }
|
242
|
+
rescue TypeError
|
243
|
+
# target.singleton_class.instance_eval fails for some instances, like symbols
|
244
|
+
end
|
245
|
+
|
246
|
+
proc
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Builds an enumerable converter Proc that converts to target, with options.
|
251
|
+
#
|
252
|
+
# Default converter is returned unless options contains a :mode (see Conversion.base_converter), and ArgumentError is raised if :mode is unsupported.
|
253
|
+
#
|
254
|
+
# See also: Conversion.converter, Object#convert_entries_to
|
255
|
+
def entries_converter(target, options={})
|
256
|
+
converter(target, options).convert_to(Conversion::EnumerableConverter)
|
257
|
+
end
|
258
|
+
|
259
|
+
# You're not supposed to call this method. Use Conversion.converter or Conversion.entries_converter instead.
|
260
|
+
#
|
261
|
+
# Returns a Proc that is the default converter to target.
|
262
|
+
#
|
263
|
+
# - If target is nil, returns nil
|
264
|
+
# - If target responds to :to_proc, returns target.to_proc
|
265
|
+
# - If target is a String, raise TypeError
|
266
|
+
# - If target is a Symbol, returns a Proc that send the target message to its parameter
|
267
|
+
# - If target is a Class, returns a Proc that creates a new object from its parameters
|
268
|
+
def base_converter(target)
|
269
|
+
converter =
|
270
|
+
# if target.respond_to?(:to_converter_proc)
|
271
|
+
# target.to_converter_proc
|
272
|
+
#
|
273
|
+
# elsif target.nil?
|
274
|
+
if target.nil?
|
275
|
+
nil
|
276
|
+
|
277
|
+
elsif target.respond_to?(:to_proc)
|
278
|
+
target.to_proc
|
279
|
+
|
280
|
+
elsif target.is_a?(String)
|
281
|
+
raise TypeError.new("Strings can't be coerced into Proc")
|
282
|
+
|
283
|
+
elsif target.is_a?(Symbol)
|
284
|
+
proc { |value| value.send(target) }
|
285
|
+
|
286
|
+
elsif target.is_a?(Class) &&
|
287
|
+
target.respond_to?(:new) &&
|
288
|
+
target.method(:new).arity != 0
|
289
|
+
proc { |*value|
|
290
|
+
if value.length == 1 && value.first.is_a?(target)
|
291
|
+
value.first
|
292
|
+
else
|
293
|
+
target.new(*value)
|
294
|
+
end
|
295
|
+
}
|
296
|
+
end
|
297
|
+
|
298
|
+
converter || raise(ArgumentError.new("#{target.inspect} can't be coerced into Proc"))
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
|
303
|
+
module EnumerableConverter #:nodoc:
|
304
|
+
class << self
|
305
|
+
# Returns a Proc that converts a converter Proc to an enumerable converter Proc
|
306
|
+
|
307
|
+
# See Conversion.converter, Object#convert_to
|
308
|
+
def to_converter_proc
|
309
|
+
proc { |converter|
|
310
|
+
converter.nil? ? nil :
|
311
|
+
proc { |value|
|
312
|
+
# challenging recursive mechanism found at http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/20469
|
313
|
+
proc { |builder| proc { |f| f.call(f) }.call( proc { |f| builder.call(proc { |*value| f.call(f).call(*value) }) }) }.call(proc { |recurse|
|
314
|
+
# the recursive proc
|
315
|
+
proc { |value|
|
316
|
+
if value.is_a?(Hash)
|
317
|
+
value.inject(value.dup.clear) { |h, (key, entry)|
|
318
|
+
h[key] = recurse.call(entry)
|
319
|
+
h
|
320
|
+
}
|
321
|
+
elsif value.is_a?(Enumerable) && !value.is_a?(String)
|
322
|
+
value.inject(value.dup.clear) { |e, entry|
|
323
|
+
e << recurse.call(entry)
|
324
|
+
}
|
325
|
+
else
|
326
|
+
converter.call(value)
|
327
|
+
end
|
328
|
+
}
|
329
|
+
}).call(value)
|
330
|
+
}
|
331
|
+
}
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
class Date
|
4
|
+
class << self
|
5
|
+
# Returns a human_input converter Proc that converts "m/d/y" dates to Date object
|
6
|
+
def to_human_input_converter_proc
|
7
|
+
proc { |value|
|
8
|
+
value = value.convert_to(Conversion::HumanInputString)
|
9
|
+
if value.nil?
|
10
|
+
nil
|
11
|
+
else
|
12
|
+
# "m/d/y" => date(y,m,d)
|
13
|
+
elements = value.split(/[-\/]/).convert_entries_to(Integer)
|
14
|
+
begin
|
15
|
+
Date.civil(elements[2], elements[0], elements[1])
|
16
|
+
rescue
|
17
|
+
value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns "m/d/y"
|
25
|
+
#
|
26
|
+
# See Conversion::HumanInputString.to_converter_proc
|
27
|
+
def to_human_input_string
|
28
|
+
# date(y,m,d) => "m/d/y"
|
29
|
+
"#{month}/#{day}/#{year}"
|
30
|
+
end
|
31
|
+
end
|