methlab 0.0.3 → 0.0.4
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/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
|
|