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 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
@@ -55,7 +55,7 @@
55
55
  #
56
56
  module MethLab
57
57
 
58
- VERSION = "0.0.3"
58
+ VERSION = "0.0.4"
59
59
 
60
60
  # Integrates MethLab into all namespaces. It does this by patching itself
61
61
  # into ::main and Module.
@@ -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
- version: 0.0.3
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.1
61
+ rubygems_version: 1.3.6
49
62
  signing_key:
50
- specification_version: 2
63
+ specification_version: 3
51
64
  summary: A method construction and validation toolkit.
52
65
  test_files: []
53
66