methlab 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README +152 -0
- data/Rakefile +65 -0
- data/lib/methlab.rb +1 -1
- data/test/test_checks.rb +145 -0
- data/test/test_integrate.rb +30 -0
- metadata +18 -5
data/README
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
Yo dawg, I heard you liked methods.
|
2
|
+
|
3
|
+
Meth Lab is a method construction toolkit intended to ease parameter
|
4
|
+
validation and a number of common "method definition patterns" seen in
|
5
|
+
ruby code.
|
6
|
+
|
7
|
+
=== SYNOPSIS
|
8
|
+
--------
|
9
|
+
|
10
|
+
A lot of times in the programming world, especially in dynamically
|
11
|
+
typed languages, we see this pattern:
|
12
|
+
|
13
|
+
def foo(arg1, arg2, arg3)
|
14
|
+
unless arg1.kind_of?(SomeObject)
|
15
|
+
raise "Incorrect object"
|
16
|
+
end
|
17
|
+
|
18
|
+
unless arg2 ...
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
You get the idea. Additionally, with the use of hashes for emulating
|
23
|
+
named parameters, we see a ton of this in the ruby world:
|
24
|
+
|
25
|
+
def bar(*args)
|
26
|
+
args = args[0]
|
27
|
+
|
28
|
+
raise "not a hash" unless args.kind_of?(Hash)
|
29
|
+
|
30
|
+
unless args.has_key?(:some_parameter)
|
31
|
+
raise "'some parameter' does not exist"
|
32
|
+
end
|
33
|
+
|
34
|
+
unless args[:some_parameter].kind_of?(SomeObject)
|
35
|
+
raise "'some parameter' is not a kind of SomeObject"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Meth Lab is intended to strip your methods of this wordy, but
|
40
|
+
important boilerplate, by making it ideally less wordy and slamming it
|
41
|
+
right in the prototype to easily evaluate what it does. Also, it has
|
42
|
+
an awesomely apropos name.
|
43
|
+
|
44
|
+
=== USAGE
|
45
|
+
--------
|
46
|
+
|
47
|
+
So, let's do something like this:
|
48
|
+
|
49
|
+
class Awesome
|
50
|
+
def_ordered(:foo, String, [Integer, :optional]) do |params|
|
51
|
+
str, int = params
|
52
|
+
puts "I received #{str} as a String and #{int} as an Integer!"
|
53
|
+
end
|
54
|
+
|
55
|
+
def_named(:bar, :foo => String, :bar => [Integer, :required]) do |params|
|
56
|
+
puts "I received #{params[:foo]} as a String and #{params[:bar]} as an Integer!"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
Which yields these opportunities:
|
61
|
+
|
62
|
+
a = Awesome.new
|
63
|
+
a.foo(1, "str") # raises
|
64
|
+
a.foo("str", 1) # prints the message
|
65
|
+
a.foo("str") # prints the message with nil as the integer
|
66
|
+
|
67
|
+
a.bar(:foo => 1, :bar => "str") # raises
|
68
|
+
a.bar(:foo => "str") # raises (:bar is required)
|
69
|
+
a.bar(:bar => 1) # prints message, with nil string
|
70
|
+
a.bar(:foo => "str", :bar => 1) # prints message
|
71
|
+
|
72
|
+
See the MethLab module documentation for details.
|
73
|
+
|
74
|
+
=== FAQ
|
75
|
+
--------
|
76
|
+
|
77
|
+
1) OMG OMG OMG OMG DUCK TYPING, you don't need this at all.
|
78
|
+
|
79
|
+
a) Then don't use it. I will offer a little opinion though: if you
|
80
|
+
think duck typing can solve everything, chances are you aren't hitting
|
81
|
+
a whole class of edge cases in your code. Also, punk rock is dead, get
|
82
|
+
with the times and think for yourself.
|
83
|
+
|
84
|
+
2) Exceptions; how do I debug validation failures?
|
85
|
+
|
86
|
+
a) Meth Lab works hard to not raise until the last possible minute for
|
87
|
+
validation failures. Generally you will see a trace like this:
|
88
|
+
|
89
|
+
(lines wrapped for sanity)
|
90
|
+
|
91
|
+
lib/methlab.rb:95:in `bar': value of argument '1' is an invalid
|
92
|
+
type. Requires 'Integer' (ArgumentError)
|
93
|
+
from lib/my-actual-file.rb:126
|
94
|
+
|
95
|
+
What's important to understand here is that, short of making you, the
|
96
|
+
user, handle raising validation errors yourself (which would kind of
|
97
|
+
defeat the point), we raise just above where your code would execute.
|
98
|
+
This means that both parts of this trace are significant; the first
|
99
|
+
line is the error and the method the error was raised from, but the
|
100
|
+
second line is the place that the method was called.
|
101
|
+
|
102
|
+
I will not even pretend this is anything other than a "misfeature",
|
103
|
+
but it is a consequence to working with a library like this.
|
104
|
+
|
105
|
+
MethLab will always raise ArgumentError, which is standard for good
|
106
|
+
ruby programs.
|
107
|
+
|
108
|
+
3) Don't you solve this with blocks? Are closures an issue?
|
109
|
+
|
110
|
+
a) The short, proper answer to this is "yes". The pragmatic answer to
|
111
|
+
this is "probably not". Closures are created and there is a
|
112
|
+
performance impact to relying on them. I would argue that if
|
113
|
+
performance is your concern, you shouldn't be using ruby at all. Meth
|
114
|
+
Lab does make every effort possible to not use proc objects where
|
115
|
+
possible, e.g., the check methods themselves live in the MethLab
|
116
|
+
namespace and are not anonymously injected into your methods (which
|
117
|
+
would be very expensive, and cause inconsistency if someone were to
|
118
|
+
redefine them).
|
119
|
+
|
120
|
+
4) I don't want to use MethLab everywhere, but I'd like to use it in
|
121
|
+
a specific class or module.
|
122
|
+
|
123
|
+
a) "extend MethLab" in your namespace before using the definition
|
124
|
+
syntax.
|
125
|
+
|
126
|
+
5) I want to use MethLab everywhere.
|
127
|
+
|
128
|
+
a) MethLab.integrate does this. If you want it to happen at require
|
129
|
+
time, set the global $METHLAB_AUTOINTEGRATE to a true value before
|
130
|
+
doing so. These both pollute ::Module and ::main, so be aware of the
|
131
|
+
consequences.
|
132
|
+
|
133
|
+
=== TODO
|
134
|
+
--------
|
135
|
+
|
136
|
+
* #respond_to? checks.
|
137
|
+
* multiplexed array/hash methods.
|
138
|
+
* construction of methods that can take inline blocks
|
139
|
+
(e.g., "def foo(&block)"). Proc can be used now, but it's
|
140
|
+
not very "rubyish" syntax for the user.
|
141
|
+
* Better handling in Modules (def self.meth(...))
|
142
|
+
|
143
|
+
=== THANKS
|
144
|
+
--------
|
145
|
+
|
146
|
+
* James Tucker (raggi) and "saywatmang" for inspiration and a bit of
|
147
|
+
opinion and code review.
|
148
|
+
* Eric Hodel (drbrain) for lots and lots of mentoring and generally being a
|
149
|
+
standup dude.
|
150
|
+
* Params::Validate and Method::Signatures from CPAN for doing it right
|
151
|
+
first. Imitation is the sincerest form of flattery and good ideas
|
152
|
+
should not be kept in an ivory tower, so prematurely shut it plox.
|
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
rescue LoadError
|
4
|
+
end
|
5
|
+
|
6
|
+
$:.unshift 'lib'
|
7
|
+
require 'methlab'
|
8
|
+
$:.shift
|
9
|
+
|
10
|
+
require 'rake/testtask'
|
11
|
+
require 'rdoc/task'
|
12
|
+
require 'rake/packagetask'
|
13
|
+
require 'rake/gempackagetask'
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = "methlab"
|
17
|
+
s.version = MethLab::VERSION
|
18
|
+
s.author = "Erik Hollensbe"
|
19
|
+
s.email = "erik@hollensbe.org"
|
20
|
+
s.summary = "A method construction and validation toolkit."
|
21
|
+
s.homepage = "http://github.com/erikh/methlab"
|
22
|
+
|
23
|
+
s.files = Dir["lib/**/*"] + Dir["test/**/*"] + Dir["Rakefile"] + Dir["README"]
|
24
|
+
|
25
|
+
s.has_rdoc = true
|
26
|
+
end
|
27
|
+
|
28
|
+
Rake::GemPackageTask.new(spec) do |s|
|
29
|
+
end
|
30
|
+
|
31
|
+
Rake::PackageTask.new(spec.name, spec.version) do |p|
|
32
|
+
p.need_tar_gz = true
|
33
|
+
p.need_zip = true
|
34
|
+
p.package_files.include("./bin/**/*")
|
35
|
+
p.package_files.include("./Rakefile")
|
36
|
+
p.package_files.include("./lib/**/*.rb")
|
37
|
+
p.package_files.include("./test/**/*")
|
38
|
+
p.package_files.include("README")
|
39
|
+
end
|
40
|
+
|
41
|
+
Rake::TestTask.new do |t|
|
42
|
+
t.libs << 'lib'
|
43
|
+
t.test_files = FileList['test/test*.rb']
|
44
|
+
t.verbose = true
|
45
|
+
end
|
46
|
+
|
47
|
+
RDoc::Task.new do |rd|
|
48
|
+
rd.rdoc_dir = "rdoc"
|
49
|
+
rd.main = "README"
|
50
|
+
rd.title = "MethLab: A method toolkit for Ruby"
|
51
|
+
rd.rdoc_files.include("./lib/**/*.rb")
|
52
|
+
rd.rdoc_files.include("README")
|
53
|
+
rd.options = %w(-a)
|
54
|
+
end
|
55
|
+
|
56
|
+
task :fixperms do
|
57
|
+
chmod(0644, Dir['**/*'])
|
58
|
+
end
|
59
|
+
|
60
|
+
task :default => [:clean, :test, :build]
|
61
|
+
desc "Build Packages"
|
62
|
+
task :build => [:gem, :repackage]
|
63
|
+
task :distclean => [:clobber_package, :clobber_rdoc]
|
64
|
+
desc "Clean the source tree"
|
65
|
+
task :clean => [:distclean]
|
data/lib/methlab.rb
CHANGED
data/test/test_checks.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
gem 'test-unit'
|
4
|
+
rescue LoadError
|
5
|
+
end
|
6
|
+
|
7
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
8
|
+
|
9
|
+
require 'test/unit'
|
10
|
+
require 'methlab'
|
11
|
+
|
12
|
+
MethLab.integrate
|
13
|
+
|
14
|
+
class CheckedClass
|
15
|
+
def_named(:named, :stuff => String, :stuff2 => [ /pee/, :required ], :stuff3 => :required) do |params|
|
16
|
+
[:stuff, :stuff2, :stuff3].collect { |x| params[x] }
|
17
|
+
end
|
18
|
+
|
19
|
+
def_ordered(:sequential, String, [Integer, :optional]) do |params|
|
20
|
+
params
|
21
|
+
end
|
22
|
+
|
23
|
+
def_ordered(:ranged, (0..9)) do |params|
|
24
|
+
params
|
25
|
+
end
|
26
|
+
|
27
|
+
def_ordered(:proc_nil, proc { |x| x.nil? }) do |params|
|
28
|
+
params
|
29
|
+
end
|
30
|
+
|
31
|
+
def_ordered(:proc_raise, proc { |x| ArgumentError.new("foo") }) do |params|
|
32
|
+
params
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
$named_proc = build_named(:stuff => String) do |params|
|
37
|
+
params
|
38
|
+
end
|
39
|
+
|
40
|
+
$ordered_proc = build_ordered((0..9)) do |params|
|
41
|
+
params
|
42
|
+
end
|
43
|
+
|
44
|
+
# FIXME module tests
|
45
|
+
|
46
|
+
class TestChecks < Test::Unit::TestCase
|
47
|
+
def setup
|
48
|
+
@checked = CheckedClass.new
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_01_named
|
52
|
+
assert(@checked.respond_to?(:named))
|
53
|
+
|
54
|
+
assert_raises(ArgumentError.new("value of argument 'stuff' is an invalid type. Requires 'String'")) do
|
55
|
+
@checked.named(:stuff => 1, :stuff2 => "pee", :stuff3 => 1)
|
56
|
+
end
|
57
|
+
|
58
|
+
assert_raises(ArgumentError.new("value of argument 'stuff2' does not match this regexp: '(?-mix:pee)'")) do
|
59
|
+
@checked.named(:stuff => "foo", :stuff2 => "bar", :stuff3 => 1)
|
60
|
+
end
|
61
|
+
|
62
|
+
assert_raises(ArgumentError.new("argument(s) 'stuff2' were not found but are required by the prototype")) do
|
63
|
+
@checked.named(:stuff => "foo", :stuff3 => 1)
|
64
|
+
end
|
65
|
+
|
66
|
+
assert_raises(ArgumentError.new("argument(s) 'stuff2, stuff3' were not found but are required by the prototype")) do
|
67
|
+
@checked.named(:stuff => "foo")
|
68
|
+
end
|
69
|
+
|
70
|
+
assert_raises(ArgumentError.new("argument(s) 'stuff2' were not found but are required by the prototype")) do
|
71
|
+
@checked.named(:stuff => "foo", :stuff3 => nil)
|
72
|
+
end
|
73
|
+
|
74
|
+
assert_equal(
|
75
|
+
@checked.named(:stuff => "foo", :stuff2 => "poopee", :stuff3 => 1),
|
76
|
+
["foo", "poopee", 1]
|
77
|
+
)
|
78
|
+
|
79
|
+
assert_equal($named_proc.call(:stuff => "foo"), { :stuff => "foo" })
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_02_checked
|
83
|
+
assert(@checked.respond_to?(:sequential))
|
84
|
+
|
85
|
+
assert_raises(ArgumentError.new("value of argument '0' is an invalid type. Requires 'String'")) do
|
86
|
+
@checked.sequential(nil)
|
87
|
+
end
|
88
|
+
|
89
|
+
assert_raises(ArgumentError.new("value of argument '1' is an invalid type. Requires 'Integer'")) do
|
90
|
+
@checked.sequential("foo", "bar")
|
91
|
+
end
|
92
|
+
|
93
|
+
assert_raises(ArgumentError.new("value of argument '1' is an invalid type. Requires 'Integer'")) do
|
94
|
+
@checked.sequential("foo", nil)
|
95
|
+
end
|
96
|
+
|
97
|
+
assert_raises(ArgumentError.new("too many arguments (3 for 2)")) do
|
98
|
+
@checked.sequential("foo", 1, nil)
|
99
|
+
end
|
100
|
+
|
101
|
+
assert_raises(ArgumentError.new("not enough arguments (0 for minimum 1)")) do
|
102
|
+
@checked.sequential()
|
103
|
+
end
|
104
|
+
|
105
|
+
assert_equal(@checked.sequential("foo"), ["foo"])
|
106
|
+
assert_equal(@checked.sequential("foo", 1), ["foo", 1])
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_03_ranges
|
110
|
+
assert(@checked.respond_to?(:ranged))
|
111
|
+
|
112
|
+
assert_raises(ArgumentError.new("value of argument '0' does not match range '0..9'")) do
|
113
|
+
@checked.ranged(-1)
|
114
|
+
end
|
115
|
+
|
116
|
+
assert_raises(ArgumentError.new("value of argument '0' does not match range '0..9'")) do
|
117
|
+
@checked.ranged("foo")
|
118
|
+
end
|
119
|
+
|
120
|
+
assert_raises(ArgumentError.new("value of argument '0' does not match range '0..9'")) do
|
121
|
+
@checked.ranged(10)
|
122
|
+
end
|
123
|
+
|
124
|
+
assert_equal(@checked.ranged(5), [5])
|
125
|
+
assert_equal($ordered_proc.call(5), [5])
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_04_procs
|
129
|
+
assert(@checked.respond_to?(:proc_nil))
|
130
|
+
|
131
|
+
assert_raises(ArgumentError.new("value of argument '0' does not pass custom validation.")) do
|
132
|
+
@checked.proc_nil(true)
|
133
|
+
end
|
134
|
+
|
135
|
+
assert_equal(@checked.proc_nil(nil), [nil])
|
136
|
+
|
137
|
+
assert(@checked.respond_to?(:proc_nil))
|
138
|
+
|
139
|
+
assert_raises(ArgumentError.new("foo")) do
|
140
|
+
@checked.proc_raise(true)
|
141
|
+
end
|
142
|
+
|
143
|
+
assert_equal(@checked.proc_nil(nil), [nil])
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
gem 'test-unit'
|
4
|
+
rescue LoadError
|
5
|
+
end
|
6
|
+
|
7
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
8
|
+
|
9
|
+
require 'test/unit'
|
10
|
+
require 'methlab'
|
11
|
+
|
12
|
+
class TestIntegrate < Test::Unit::TestCase
|
13
|
+
def test_01_integration
|
14
|
+
main = eval("self", TOPLEVEL_BINDING)
|
15
|
+
MethLab.integrate
|
16
|
+
|
17
|
+
module_methods = Module.instance_methods
|
18
|
+
main_methods = main.methods
|
19
|
+
|
20
|
+
assert(module_methods.include?(:def_named) || module_methods.include?("def_named"))
|
21
|
+
assert(module_methods.include?(:def_ordered) || module_methods.include?("def_ordered"))
|
22
|
+
assert(module_methods.include?(:build_named) || module_methods.include?("build_named"))
|
23
|
+
assert(module_methods.include?(:build_ordered) || module_methods.include?("build_ordered"))
|
24
|
+
|
25
|
+
assert(main_methods.include?(:def_named) || main_methods.include?("def_named"))
|
26
|
+
assert(main_methods.include?(:def_ordered) || main_methods.include?("def_ordered"))
|
27
|
+
assert(main_methods.include?(:build_named) || main_methods.include?("build_named"))
|
28
|
+
assert(main_methods.include?(:build_ordered) || main_methods.include?("build_ordered"))
|
29
|
+
end
|
30
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: methlab
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 4
|
9
|
+
version: 0.0.4
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Erik Hollensbe
|
@@ -23,8 +28,14 @@ extra_rdoc_files: []
|
|
23
28
|
|
24
29
|
files:
|
25
30
|
- lib/methlab.rb
|
31
|
+
- test/test_checks.rb
|
32
|
+
- test/test_integrate.rb
|
33
|
+
- Rakefile
|
34
|
+
- README
|
26
35
|
has_rdoc: true
|
27
36
|
homepage: http://github.com/erikh/methlab
|
37
|
+
licenses: []
|
38
|
+
|
28
39
|
post_install_message:
|
29
40
|
rdoc_options: []
|
30
41
|
|
@@ -34,20 +45,22 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
34
45
|
requirements:
|
35
46
|
- - ">="
|
36
47
|
- !ruby/object:Gem::Version
|
48
|
+
segments:
|
49
|
+
- 0
|
37
50
|
version: "0"
|
38
|
-
version:
|
39
51
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
52
|
requirements:
|
41
53
|
- - ">="
|
42
54
|
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
43
57
|
version: "0"
|
44
|
-
version:
|
45
58
|
requirements: []
|
46
59
|
|
47
60
|
rubyforge_project:
|
48
|
-
rubygems_version: 1.3.
|
61
|
+
rubygems_version: 1.3.6
|
49
62
|
signing_key:
|
50
|
-
specification_version:
|
63
|
+
specification_version: 3
|
51
64
|
summary: A method construction and validation toolkit.
|
52
65
|
test_files: []
|
53
66
|
|