conversion 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|