detach 0.0.1
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.
- checksums.yaml +7 -0
- data/lib/detach.rb +106 -0
- data/test/test_detach.rb +142 -0
- metadata +44 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b5f072cc4f0f35c933ba67ede12f20dbd8bcb258
|
4
|
+
data.tar.gz: f442b9baf99c44c4a49ec2f8af5bc6bce365f50a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9d6f1eeb94dc88d3db16390ef1c802fb9421bee7c4fe17d06fce3ca7e3b9a50c247c7599b82254e34090d558f76d31d36f94c76968aed339557a5645a6ba54f6
|
7
|
+
data.tar.gz: 0bddec8616d667425cdae4b0c6c775cc8dadea9b863bc307125ddfd068bdeaa788fcd0b88a0660e948aeb61820f870a09514f56d0c6df2d0d5a86c935ffe0db0
|
data/lib/detach.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# The Detach mixin provides method dispatch according to argument types.
|
2
|
+
# Method definitions are separated by name and signatue, allowing for
|
3
|
+
# C++ or Java style overloading.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# class Bar
|
7
|
+
# include Detach
|
8
|
+
# taking['String','String']
|
9
|
+
# def foo(a,b)
|
10
|
+
# a.upcase + b.upcase
|
11
|
+
# end
|
12
|
+
# taking['Integer','Integer']
|
13
|
+
# def foo(a=42,b)
|
14
|
+
# a * b
|
15
|
+
# end
|
16
|
+
# taking['Object','String']
|
17
|
+
# def foo(a,*b)
|
18
|
+
# b.map {|s| s.upcase + a.to_s}.join
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
module Detach
|
22
|
+
# Extends the base class with the module Detach::Types.
|
23
|
+
def self.included(base)
|
24
|
+
base.extend(Types)
|
25
|
+
end
|
26
|
+
# Provides run-time method lookup according to the types of the args.
|
27
|
+
#
|
28
|
+
# All methods matching the name are scored according to both arity and type.
|
29
|
+
# Varargs and default values are interpolated with actual values. Predefined classes
|
30
|
+
# are compared to actual classes using equality and inheritence checks.
|
31
|
+
def method_missing(name, *args, &block)
|
32
|
+
(score,best) = (public_methods+protected_methods+private_methods).grep(/^#{name}\(/).collect {|candidate|
|
33
|
+
# extract paramters
|
34
|
+
params = /\((.*)\)/.match(candidate.to_s)[1].scan(/(\w+)-([\w:)]+)/).collect {|s,t|
|
35
|
+
[s.to_sym, t.split(/::/).inject(Kernel) {|m,c| m = m.const_get(c)}]
|
36
|
+
}
|
37
|
+
# form the list of all required argument classes
|
38
|
+
ctypes = params.values_at(*params.each_index.select {|i| params[i].first == :req}).map(&:last)
|
39
|
+
|
40
|
+
# NOTE: ruby only allows a single *args, or a list of a=1, b=2--not both together--
|
41
|
+
# only one of the following will execute
|
42
|
+
|
43
|
+
# (A) insert any optional argument classes for as many extra are present
|
44
|
+
params.each_index.select {|i| params[i].first == :opt}.each {|i|
|
45
|
+
ctypes.insert(i, params[i].last) if args.size > ctypes.size
|
46
|
+
}
|
47
|
+
# (B) insert the remaining arguments by exploding the appropriate class by the number extra
|
48
|
+
params.each_index.select {|i| params[i].first == :rest}.each {|i|
|
49
|
+
ctypes.insert(i, *([params[i].last] * (args.size - ctypes.size))) if args.size > ctypes.size
|
50
|
+
}
|
51
|
+
|
52
|
+
# now score the given args by comparing their actual classes to the predefined classes
|
53
|
+
if args.empty? and ctypes.empty?
|
54
|
+
score = 1
|
55
|
+
elsif ctypes.size == args.size
|
56
|
+
score = args.map(&:class).zip(ctypes).inject(0) {|s,t|
|
57
|
+
# apply each class comparison and require nonzero matches
|
58
|
+
s and ->(n) {s += n if n > 0}[ [ :==, :<=, ].select {|op| ->(a,&b) {b[*a]}[t, &op]}.size ]
|
59
|
+
} || 0
|
60
|
+
else
|
61
|
+
score = 0
|
62
|
+
end
|
63
|
+
|
64
|
+
[ score, candidate ]
|
65
|
+
|
66
|
+
}.max {|a,b| a[0] <=> b[0]}
|
67
|
+
|
68
|
+
(not score or score == 0) ? super : method(best)[*args, &block]
|
69
|
+
end
|
70
|
+
|
71
|
+
# The Detach::Types module is inserted as a parent of the class which includes the
|
72
|
+
# Detach mixin. This module handles inspection and aliasing of instance methods
|
73
|
+
# as they are added.
|
74
|
+
#
|
75
|
+
# Detach::Types does not need to be extended directly.
|
76
|
+
module Types
|
77
|
+
# Decorator method for defining argument signature.
|
78
|
+
#
|
79
|
+
# Example:
|
80
|
+
# taking['String']
|
81
|
+
# def foo(a)
|
82
|
+
# end
|
83
|
+
def taking
|
84
|
+
self
|
85
|
+
end
|
86
|
+
# :stopdoc:
|
87
|
+
def [](*types)
|
88
|
+
@@types = types.flatten
|
89
|
+
end
|
90
|
+
def method_added(name)
|
91
|
+
return unless @@types
|
92
|
+
|
93
|
+
# query the parameter info for the method just added
|
94
|
+
p = instance_method(name).parameters.map &:first
|
95
|
+
raise ArgumentError.new('type and parameter mismatch') unless p.size == @@types.size
|
96
|
+
|
97
|
+
# encode our defined types with parameter info into a new name and remove the original
|
98
|
+
n = (name.to_s + '(' + p.zip(@@types).collect {|p,t| "#{p}-#{t}" }.join(',') + ')').to_sym
|
99
|
+
@@types = nil
|
100
|
+
|
101
|
+
alias_method n, name unless method_defined?(n)
|
102
|
+
define_method(name) {|*args, &block| method_missing(name, *args, &block)}
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
data/test/test_detach.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'detach'
|
3
|
+
|
4
|
+
class TestDetach < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def testBasicTypes
|
7
|
+
assert_equal "String hello", Foo.new.foo('hello')
|
8
|
+
assert_equal "Integer 42", Foo.new.foo(42)
|
9
|
+
assert_equal "Strings helloworld", Foo.new.foo('hello', 'world')
|
10
|
+
assert_equal "Integers 42", Foo.new.foo(40, 2)
|
11
|
+
assert_equal "Floats 4.7", Foo.new.foo(3.1, 1.6)
|
12
|
+
assert_equal "Numbers 4.7", Foo.new.foo(3, 1.7)
|
13
|
+
end
|
14
|
+
|
15
|
+
def testNoArgs
|
16
|
+
assert_equal "Nonce", Foo.new.foo
|
17
|
+
end
|
18
|
+
|
19
|
+
def testLists
|
20
|
+
assert_equal "Object List [\"one\", 2, 3.0, \"4\"]", Foo.new.foo('one', 2, 3.0, '4')
|
21
|
+
assert_equal "String List [\"one\", \"two\", \"three\", \"four\"]", Foo.new.foo('one', 'two', 'three', 'four')
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def testMixedLists
|
26
|
+
assert_equal "Int-String List-String 1 [] guy", Foo.new.foo(1, 'guy')
|
27
|
+
assert_equal "Int-String List-String 1 [\"is\", \"the\", \"lonliest\"] number", Foo.new.foo(1, 'is', 'the', 'lonliest', 'number')
|
28
|
+
end
|
29
|
+
|
30
|
+
def testOptional
|
31
|
+
assert_equal "Int-String-Int 7 ate 9", Foo.new.foo(7, 'ate', 9)
|
32
|
+
assert_equal "Int-String-Int 42 < 99", Foo.new.foo('<', 99)
|
33
|
+
end
|
34
|
+
|
35
|
+
def testCustomObject
|
36
|
+
assert_equal "P::M 42", Foo.new.foo(P::M.new)
|
37
|
+
end
|
38
|
+
|
39
|
+
def testConstructor
|
40
|
+
assert_equal "hello world", Wow.new("hello", "world").to_s
|
41
|
+
assert_equal "11", Wow.new(5, 6).to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
#
|
46
|
+
# helper definitions
|
47
|
+
#
|
48
|
+
|
49
|
+
module P
|
50
|
+
class M
|
51
|
+
def wow
|
52
|
+
42
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Foo
|
58
|
+
include Detach
|
59
|
+
|
60
|
+
taking[String, String]
|
61
|
+
def foo(a, b)
|
62
|
+
"Strings #{a + b}"
|
63
|
+
end
|
64
|
+
|
65
|
+
taking[Integer, Integer]
|
66
|
+
def foo(a, b)
|
67
|
+
"Integers #{a + b}"
|
68
|
+
end
|
69
|
+
|
70
|
+
taking[Float, Float]
|
71
|
+
def foo(a, b)
|
72
|
+
"Floats #{a + b}"
|
73
|
+
end
|
74
|
+
|
75
|
+
taking[Numeric, Numeric]
|
76
|
+
def foo(a, b)
|
77
|
+
"Numbers #{a + b}"
|
78
|
+
end
|
79
|
+
|
80
|
+
taking[String]
|
81
|
+
def foo(a)
|
82
|
+
"String #{a}"
|
83
|
+
end
|
84
|
+
|
85
|
+
taking[Integer]
|
86
|
+
def foo(a)
|
87
|
+
"Integer #{a}"
|
88
|
+
end
|
89
|
+
|
90
|
+
taking[]
|
91
|
+
def foo
|
92
|
+
"Nonce"
|
93
|
+
end
|
94
|
+
|
95
|
+
taking[Object]
|
96
|
+
def foo(*a)
|
97
|
+
"Object List #{a}"
|
98
|
+
end
|
99
|
+
|
100
|
+
taking[String]
|
101
|
+
def foo(*a)
|
102
|
+
"String List #{a}"
|
103
|
+
end
|
104
|
+
|
105
|
+
taking[Integer, String, String]
|
106
|
+
def foo(a, *b, c)
|
107
|
+
"Int-String List-String #{a} #{b} #{c}"
|
108
|
+
end
|
109
|
+
|
110
|
+
taking[Integer, String, Integer]
|
111
|
+
def foo(a=42, b, c)
|
112
|
+
"Int-String-Int #{a} #{b} #{c}"
|
113
|
+
end
|
114
|
+
|
115
|
+
taking[P::M]
|
116
|
+
def foo(m)
|
117
|
+
"P::M #{m.wow}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Wow
|
122
|
+
include Detach
|
123
|
+
|
124
|
+
taking[String,String]
|
125
|
+
def initialize(a,b)
|
126
|
+
@s = "#{a} #{b}"
|
127
|
+
end
|
128
|
+
|
129
|
+
taking[Integer,Integer]
|
130
|
+
def initialize(a,b)
|
131
|
+
@s = a + b
|
132
|
+
end
|
133
|
+
|
134
|
+
def to_s
|
135
|
+
@s.to_s
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
|
metadata
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: detach
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Calhoun
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-19 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/detach.rb
|
20
|
+
- test/test_detach.rb
|
21
|
+
homepage:
|
22
|
+
licenses: []
|
23
|
+
metadata: {}
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
requirements: []
|
39
|
+
rubyforge_project:
|
40
|
+
rubygems_version: 2.2.2
|
41
|
+
signing_key:
|
42
|
+
specification_version: 4
|
43
|
+
summary: Separate methods by argument types
|
44
|
+
test_files: []
|