From: Florian Gross Date: 2004-08-19T04:00:58+09:00 Subject: Re: Documenting a class interface when there are no types in the method signature --------------060609010605040107040402 Content-Type: text/plain; charset=us-ascii; format=flowed Content-Transfer-Encoding: 7bit Nicolai Czempin wrote: > When I define a method in a class, let's say initialize(categories, data) > for the sake of argument: In Java etc. I can see from the method definition > that categories is an ordered set, and data is a Map (and if that info is > not sufficient, javadoc can be used to explain in more detail what is > expected, but I'll leave that out for the moment, it's not really key to the > question). > Of course the first step would be to find a good name (at the very least > "data" is a bit too generic). But what next? How does someone who writes an > API communicate that it makes no sense to send "1" to the categories. > Apparently the unit tests and/or dbc encompass the specification, but does > rdoc or some tool extract any information from them? No, RDoc itself can't extract information from Unit Tests. I'm right now commonly embedding some simple unit tests into my documentation which also act as handy sample code. Like in this -- bad and made-up -- example: # Returns the first of the two halves of an Object. # This works with all Objects that respond to the #[] and #size methods. # # Example code: # halve("hello world") # => "hello" # halve([1, 2, 3, 4]) # => [1, 2] # halve(Object.new) # raises NoMethodError def halve(obj) obj[0, obj.size / 2] end I have written a small tool that will extract the embedded sample code and run it as unit tests. It's still not perfect, but I've attached it to this mail -- maybe it is of some use for you. Here's a real world example: (from the evil-ruby project) > # Unfreeze a frozen Object. You will be able to make > # changes to the object again. > # > # obj = "Hello World".freeze > # obj.frozen? # => true > # obj.unfreeze > # obj.frozen? # => false > # obj.sub!("World", "You!") > # obj # => "Hello You!" > def unfreeze > if $SAFE > 0 > raise(SecurityError, "Insecure operation `unfreeze' at level #{$SAFE}") > end > > return self if direct_value? > > self.internal.flags &= ~RubyInternal::FL_FREEZE > return self > end So my answer to the question "How does a library user know what protocol objects need to comply with when they are used as arguments for method X?" is "It's specified in the documentation of method X. There's also sample code in there which will yield failing unit test when the protocol changes so the documentation won't be outdated." I hope I could answer at least some of your questions. Regards, Florian Gross --------------060609010605040107040402 Content-Type: text/plain; name="extract.rb" Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="extract.rb" require 'test/unit/testcase' require 'test/unit/ui/console/testrunner' class Extracter def self.process(fn) new(File.read(fn)) end def initialize(content) comment_block_re = /((?:^\s*?(?:#.*?)?\n)+)/m component_re = /\s*(?:class|def|module|alias)\s+:?([^\s()]+)?/ blocks = content.scan(/#{comment_block_re}#{component_re}/) test_suite = Class.new(Test::Unit::TestCase) has_test = false blocks.each do |(comment, component)| code_in_doc_re = /^(\s*# +(?:.*?)$)/ tests = comment.scan(code_in_doc_re) body = tests.map do |test| test.map do |raw_line| line = raw_line.sub(/^\s*#\s{0,3}/, "") if md = /(.*?)#\s*(=>|~>|raises?)\s*(.*?)$/.match(line) new_line, type, result = *md.captures new_line.strip! case type when "=>" ["begin", " assert_equal(#{result}, #{new_line})", "rescue => err", " assert_equal(#{result.inspect}, (#{new_line}).inspect)", "end"].join("\n") when "~>", "raise", "raises" "assert_raises(Object.const_get(#{result.inspect})) { #{new_line} }" end else line end end.join("\n") end.join("\n") unless component if $DEBUG STDERR.puts "Can't get name for this code:", body.gsub(/(?:\r?\n){2}/, "\n") end component = test.hash.abs end if body and not body.empty? has_test = true test_suite.class_eval %{ def #{test_method_name(component)} #{body} end } end end if not has_test test_suite.class_eval do def test_main; end end end Test::Unit::UI::Console::TestRunner.new(test_suite).start end def test_method_name(component) result = "test_#{component}" { "+" => "op_plus", "-" => "op_minus", "+@" => "op_plus_self", "-@" => "op_minus_self", "*" => "op_mul", "**" => "op_pow", "/" => "op_div", "%" => "op_mod", "<<" => "op_lshift", ">>" => "op_rshift", "~" => "op_tilde", "<=>" => "op_cmp", "<" => "op_lt", ">" => "op_gt", "==" => "op_equal", "<=" => "op_lt_eq", ">=" => "op_gt_eq", "===" => "op_case_eq", "=~" => "op_apply", "|" => "op_or", "&" => "op_and", "^" => "op_xor", "[]" => "op_fetch", "[]=" => "op_store" }.each do |(what, by)| result.gsub!(what, by) end return result end end if __FILE__ == $0 file = ARGV.shift load(file) Extracter.process(file) end --------------060609010605040107040402--