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 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