├── .gitignore ├── Gemfile ├── HISTORY.md ├── LICENSE ├── README.md ├── Rakefile ├── bin └── tomdoc ├── lib ├── tomdoc.rb └── tomdoc │ ├── arg.rb │ ├── cli.rb │ ├── generator.rb │ ├── method.rb │ ├── reporters │ ├── base.rb │ ├── console.rb │ └── html.rb │ ├── scope.rb │ ├── source_parser.rb │ ├── tomdoc.rb │ └── version.rb ├── man ├── tomdoc.5 ├── tomdoc.5.html └── tomdoc.5.ronn ├── test ├── console_reporter_test.rb ├── fixtures │ ├── chimney.rb │ ├── multiplex.rb │ └── simple.rb ├── generator_test.rb ├── helper.rb ├── html_reporter_test.rb ├── source_parser_test.rb └── tomdoc_parser_test.rb ├── tomdoc.gemspec ├── tomdoc.md └── try └── example.rb /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | pkg 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem "ruby_parser", ">= 3.0.0" 4 | gem "colored", ">= 1.2" 5 | 6 | group :test do 7 | gem "minitest", ">= 1.5.0" 8 | gem "turn" 9 | end 10 | 11 | group :development do 12 | gem "rake", ">= 0.8.7" 13 | gem "rubyforge", ">= 2.0.3" 14 | end 15 | 16 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | = Release History 2 | 3 | == 0.2.5 | 2012-03-02 4 | 5 | This release simply fixes an issue with parsing documentation 6 | that has only a single line. 7 | 8 | Changes: 9 | 10 | * Fix indentation stripping. 11 | 12 | 13 | == 0.2.4 | 2012-02-22 14 | 15 | This release rewrites the TomDoc comment parser to add latest 16 | features of TomDoc standard. 17 | 18 | Changes: 19 | 20 | * Add support for Signature. 21 | * Add support for Raises. 22 | * Unify comment parsing into single pass. 23 | * Input text can be with or without comment marker. 24 | 25 | 26 | == 0.2.3 | 2011-06-10 27 | 28 | This release simply fix a bug that was accidently introduced 29 | in the last release, where by the comment markers are not 30 | stripped from the comment text. 31 | 32 | Changes: 33 | 34 | * Fix TomDoc#tomdoc needs to strip comments markers. 35 | 36 | 37 | == 0.2.2 | 2011-06-09 38 | 39 | Have `tomdoc/tomdoc.rb` require `tomdoc`arg.rb` which it needs. 40 | This is so plugins like yard-tomdoc can require `tomdoc/tomdoc` 41 | without the rest of tomdoc code base. 42 | 43 | Changes: 44 | 45 | * tomdoc/tomdoc.rb reuqires `tomdoc/arg.rb`. 46 | * fix bug in #tomdoc method clean code. 47 | * remark out clean code for the time being. 48 | 49 | 50 | == 0.2.1 | 2011-06-08 51 | 52 | This release removes unused dependencies from the gem. 53 | 54 | Changes: 55 | 56 | * Remove unused dependencies for gem. 57 | 58 | 59 | == 0.2.0 | 2011-05-20 60 | 61 | This release addresses a few nagging bugs and a dependency conflict. 62 | 63 | Changes: 64 | 65 | * Fix Ruby 1.9 issue, use Array#join instead of Array#to_s 66 | * Fix issue #5, undefined instance_methods 67 | * Fix Test namespace reference 68 | * Fix colored gem as dependency in gemspec 69 | * Fix TomDoc#args returns empty array when no arguments are in comment 70 | * Fix dependecy version constraints 71 | 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Tom Preston-Werner, Chris Wanstrath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | Software), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TomDoc 2 | ====== 3 | 4 | TomDoc is documentation for humans. Using a few simple rules and zero 5 | special syntax you can produce great looking documentation for both humans 6 | and machines. 7 | 8 | Just follow these four easy steps: 9 | 10 | 1. Describe your method 11 | 2. Optionally list and describe its arguments 12 | 3. Optionally list some examples 13 | 4. Explain what your method returns 14 | 15 | Like this: 16 | 17 | # Duplicate some text an arbitrary number of times. 18 | # 19 | # text - The String to be duplicated. 20 | # count - The Integer number of times to duplicate the text. 21 | # 22 | # Examples 23 | # multiplex('Tom', 4) 24 | # # => 'TomTomTomTom' 25 | # 26 | # Returns the duplicated String. 27 | def multiplex(text, count) 28 | text * count 29 | end 30 | 31 | See [the manual][man] or [the spec][spec] for a more in-depth 32 | analysis. 33 | 34 | 35 | tomdoc.rb 36 | --------- 37 | 38 | This repository contains tomdoc.rb, a Ruby library for parsing 39 | TomDoc and generating pretty documentation from it. 40 | 41 | 42 | Installation 43 | ------------ 44 | 45 | easy_install Pygments 46 | gem install tomdoc 47 | 48 | tomdoc.rb has been tested with Ruby 1.8.7. 49 | 50 | 51 | Usage 52 | ----- 53 | 54 | $ tomdoc file.rb 55 | # Prints colored documentation of file.rb. 56 | 57 | $ tomdoc file.rb -n STRING 58 | # Prints methods or classes in file.rb matching STRING. 59 | 60 | $ tomdoc fileA.rb fileB.rb ... 61 | # Prints colored documentation of multiple files. 62 | 63 | $ tomdoc -f html file.rb 64 | # Prints HTML documentation of file.rb. 65 | 66 | $ tomdoc -i file.rb 67 | # Ignore TomDoc validation, print any methods we find. 68 | 69 | $ tomdoc -h 70 | # Displays more options. 71 | 72 | 73 | Ruby API 74 | -------- 75 | 76 | Fully TomDoc'd. Well, it will be. 77 | 78 | For now: 79 | 80 | $ tomdoc lib/tomdoc/source_parser.rb 81 | 82 | 83 | Formats 84 | ------- 85 | 86 | ### Console 87 | 88 | tomdoc lib/tomdoc/source_parser.rb -n token 89 | 90 | ![pattern](http://img.skitch.com/20100408-mnyxuxb4xrrg5x4pnpsmuth4mu.png) 91 | 92 | ### HTML 93 | 94 | tomdoc -f html lib/tomdoc/source_parser.rb | browser 95 | 96 | or 97 | 98 | tomdoc -f html lib/tomdoc/source_parser.rb > doc.html 99 | open doc.html 100 | 101 | ![html](http://img.skitch.com/20100408-dbhtc4mef2q3ygmn63csxgh14w.png) 102 | 103 | Local Dev 104 | --------- 105 | 106 | Want to hack on tomdoc.rb? Of course you do. 107 | 108 | git clone http://github.com/defunkt/tomdoc.git 109 | cd tomdoc 110 | bundle install --local 111 | ruby -rubygems ./bin/tomdoc lib/tomdoc/source_parser.rb 112 | 113 | [man]: https://github.com/defunkt/tomdoc/blob/tomdoc.rb/man/tomdoc.5.ronn 114 | [spec]: https://github.com/defunkt/tomdoc/blob/tomdoc.rb/tomdoc.md 115 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/lib/tomdoc/version' 2 | 3 | require 'rake/testtask' 4 | 5 | def command?(command) 6 | system("type #{command} > /dev/null 2>&1") 7 | end 8 | 9 | # 10 | # Manual 11 | # 12 | 13 | if command? :ronn 14 | desc "Build and display the manual." 15 | task :man => "man:build" do 16 | exec "man man/tomdoc.5" 17 | end 18 | 19 | desc "Build and display the manual in your browser." 20 | task "man:html" => "man:build" do 21 | sh "open man/tomdoc.5.html" 22 | end 23 | 24 | desc "Build the manual" 25 | task "man:build" do 26 | sh "ronn -br5 --organization=MOJOMBO --manual='TomDoc Manual' man/*.ronn" 27 | end 28 | end 29 | 30 | 31 | # 32 | # Tests 33 | # 34 | 35 | task :default => :test 36 | 37 | if command? :turn 38 | desc "Run tests with turn" 39 | task :turn do 40 | suffix = "-n #{ENV['TEST']}" if ENV['TEST'] 41 | sh "turn -Ilib:. test/*.rb #{suffix}" 42 | end 43 | end 44 | 45 | Rake::TestTask.new do |t| 46 | t.libs << 'lib' 47 | t.libs << '.' 48 | t.pattern = 'test/**/*_test.rb' 49 | t.verbose = false 50 | end 51 | 52 | 53 | # 54 | # Development 55 | # 56 | 57 | desc "Drop to irb." 58 | task :console do 59 | exec "irb -I lib -rtomdoc" 60 | end 61 | 62 | 63 | # 64 | # Gems 65 | # 66 | 67 | desc "Build gem." 68 | task :gem do 69 | sh "gem build tomdoc.gemspec" 70 | end 71 | 72 | task :push => [:gem] do 73 | file = Dir["*-#{TomDoc::VERSION}.gem"].first 74 | sh "gem push #{file}" 75 | end 76 | 77 | desc "tag version" 78 | task :tag do 79 | sh "git tag v#{TomDoc::VERSION}" 80 | sh "git push origin master --tags" 81 | sh "git clean -fd" 82 | end 83 | 84 | desc "tag version and push gem to server" 85 | task :release => [:push, :tag] do 86 | puts "And away she goes!" 87 | end 88 | 89 | desc "Do nothing." 90 | task :noop do 91 | puts "Done nothing." 92 | end 93 | -------------------------------------------------------------------------------- /bin/tomdoc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.dirname(__FILE__) + '/../lib' 4 | require 'tomdoc' 5 | 6 | # Process options 7 | TomDoc::CLI.parse_options(ARGV) 8 | -------------------------------------------------------------------------------- /lib/tomdoc.rb: -------------------------------------------------------------------------------- 1 | require 'pp' 2 | 3 | require 'ruby_parser' 4 | require 'colored' 5 | 6 | module TomDoc 7 | autoload :Open3, 'open3' 8 | 9 | autoload :CLI, 'tomdoc/cli' 10 | 11 | autoload :Scope, 'tomdoc/scope' 12 | autoload :Method, 'tomdoc/method' 13 | autoload :Arg, 'tomdoc/arg' 14 | autoload :TomDoc, 'tomdoc/tomdoc' 15 | 16 | autoload :SourceParser, 'tomdoc/source_parser' 17 | autoload :Generator, 'tomdoc/generator' 18 | 19 | autoload :VERSION, 'tomdoc/version' 20 | 21 | module Reporters 22 | autoload :Base, 'tomdoc/reporters/base' 23 | autoload :Console, 'tomdoc/reporters/console' 24 | autoload :HTML, 'tomdoc/reporters/html' 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/tomdoc/arg.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | class Arg 3 | attr_accessor :name, :description 4 | 5 | # Create new Arg object. 6 | # 7 | # name - name of argument 8 | # description - arguments description 9 | # 10 | def initialize(name, description = '') 11 | @name = name.to_s.intern 12 | @description = description 13 | end 14 | 15 | # Is this an optional argument? 16 | # 17 | # Returns Boolean. 18 | def optional? 19 | @description.downcase.include? 'optional' 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/tomdoc/cli.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | module TomDoc 4 | class CLI 5 | # 6 | # DSL Magics 7 | # 8 | 9 | def self.options(&block) 10 | block ? (@options = block) : @options 11 | end 12 | 13 | def options 14 | OptionParser.new do |opts| 15 | opts.instance_eval(&self.class.options) 16 | end 17 | end 18 | 19 | def pp(*args) 20 | require 'pp' 21 | super 22 | end 23 | 24 | 25 | # 26 | # Define Options 27 | # 28 | 29 | options do 30 | @options = {} 31 | 32 | self.banner = "Usage: tomdoc [options] FILE1 FILE2 ..." 33 | 34 | separator " " 35 | separator "Examples:" 36 | separator < true 16 | } 17 | @options.update(options) 18 | 19 | @report = @options.fetch(:report, Reporters::Console) 20 | @report = @report.new(self) 21 | 22 | @scopes = {} 23 | end 24 | 25 | def self.generate(text_or_sexp) 26 | new.generate(text_or_sexp) 27 | end 28 | 29 | def generate(text_or_sexp) 30 | if text_or_sexp.is_a?(String) 31 | sexp = SourceParser.parse(text_or_sexp) 32 | else 33 | sexp = text_or_sexp 34 | end 35 | 36 | process(sexp) 37 | end 38 | 39 | def process(scopes = {}, prefix = nil) 40 | old_scopes = @scopes 41 | @scopes = scopes 42 | scopes.each do |name, scope| 43 | write_scope(scope, prefix) 44 | process(scope, "#{name}::") 45 | end 46 | 47 | report.output 48 | ensure 49 | @scopes = old_scopes || {} 50 | end 51 | 52 | def write_scope(scope, prefix) 53 | report.write_scope_header(scope, prefix) 54 | 55 | report.before_all_methods(scope, prefix) 56 | write_class_methods(scope, prefix) 57 | write_instance_methods(scope, prefix) 58 | report.after_all_methods(scope, prefix) 59 | 60 | report.write_scope_footer(scope, prefix) 61 | end 62 | 63 | def write_class_methods(scope, prefix = nil) 64 | prefix ="#{prefix}#{scope.name}." 65 | 66 | scope.class_methods.map do |method| 67 | next if !valid?(method, prefix) 68 | report.write_method(method, prefix) 69 | end.compact 70 | end 71 | 72 | def write_instance_methods(scope, prefix = nil) 73 | prefix = "#{prefix}#{scope.name}#" 74 | 75 | scope.instance_methods.map do |method| 76 | next if !valid?(method, prefix) 77 | report.write_method(method, prefix) 78 | end.compact 79 | end 80 | 81 | def pygments(text, *args) 82 | out = '' 83 | 84 | Open3.popen3("pygmentize", *args) do |stdin, stdout, stderr| 85 | stdin.puts text 86 | stdin.close 87 | out = stdout.read.chomp 88 | end 89 | 90 | out 91 | end 92 | 93 | def constant?(const) 94 | const = const.split('::').first if const.include?('::') 95 | constant_names.include?(const.intern) || Object.const_defined?(const) 96 | end 97 | 98 | def constant_names 99 | name = @scopes.name if @scopes.respond_to?(:name) 100 | [ :Boolean, :Test, name ].compact + @scopes.keys 101 | end 102 | 103 | def valid?(object, prefix) 104 | matches_pattern?(prefix, object.name) && valid_tomdoc?(object.tomdoc) 105 | end 106 | 107 | def matches_pattern?(prefix, name) 108 | if pattern = options[:pattern] 109 | # "-n hey" vs "-n /he.+y/" 110 | if pattern =~ /^\/.+\/$/ 111 | pattern = pattern.sub(/^\//, '').sub(/\/$/, '') 112 | regexp = Regexp.new(pattern) 113 | else 114 | regexp = Regexp.new(Regexp.escape(pattern)) 115 | end 116 | 117 | regexp =~ name.to_s || regexp =~ prefix.to_s 118 | else 119 | true 120 | end 121 | end 122 | 123 | def valid_tomdoc?(comment) 124 | options[:validate] ? TomDoc.valid?(comment) : true 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/tomdoc/method.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | # A Method can be instance or class level. 3 | class Method 4 | attr_accessor :name, :comment, :args 5 | 6 | def initialize(name, comment = '', args = []) 7 | @name = name 8 | @comment = comment 9 | @args = args || [] 10 | end 11 | alias_method :to_s, :name 12 | 13 | def tomdoc 14 | @tomdoc ||= TomDoc.new(@comment) 15 | end 16 | 17 | def inspect 18 | "#{name}(#{args.join(', ')})" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tomdoc/reporters/base.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | module Reporters 3 | class Base 4 | 5 | def initialize(generator) 6 | @generator = generator 7 | @buffer = "" 8 | end 9 | 10 | def write(*things) 11 | things.each do |thing| 12 | @buffer << "#{thing}\n" 13 | end 14 | nil 15 | end 16 | 17 | def output 18 | @buffer 19 | end 20 | 21 | def write_scope_header(scope, prefix = '') 22 | # Optional Implementation 23 | end 24 | 25 | def before_all_methods(scope, prefix) 26 | # Optional Implementation 27 | end 28 | 29 | def write_method(method, prefix = '') 30 | raise NotImplementedError 31 | end 32 | 33 | def after_all_methods(scope, prefix) 34 | # Optional Implementation 35 | end 36 | 37 | def write_scope_footer(scope, prefix) 38 | # Optional Implementation 39 | end 40 | 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/tomdoc/reporters/console.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | module Reporters 3 | class Console < Base 4 | 5 | def write_scope_header(scope, prefix = '') 6 | return if scope.tomdoc.to_s.empty? 7 | write_method(scope, prefix) 8 | end 9 | 10 | def write_method(method, prefix = '') 11 | write '-' * 80 12 | write "#{prefix}#{method.name}#{args(method)}".bold, '' 13 | write format_comment(method.tomdoc) 14 | end 15 | 16 | protected 17 | 18 | def args(method) 19 | return '' if !method.respond_to?(:args) 20 | if method.args.any? 21 | '(' + method.args.join(', ') + ')' 22 | else 23 | '' 24 | end 25 | end 26 | 27 | def highlight(text) 28 | @generator.pygments(text, '-l', 'ruby') 29 | end 30 | 31 | def format_comment(comment) 32 | comment = comment.to_s 33 | 34 | # Strip leading comments 35 | comment.gsub!(/^# ?/, '') 36 | 37 | # Example code 38 | comment.gsub!(/(\s*Examples\s*(.+?)\s*Returns)/m) do 39 | $1.sub($2, highlight($2)) 40 | end 41 | 42 | # Param list 43 | comment.gsub!(/^(\s*(\w+) +- )/) do 44 | param = $2 45 | $1.sub(param, param.green) 46 | end 47 | 48 | # true/false/nil 49 | comment.gsub!(/(true|false|nil)/, '\1'.magenta) 50 | 51 | # Strings 52 | comment.gsub!(/('.+?')/, '\1'.yellow) 53 | comment.gsub!(/(".+?")/, '\1'.yellow) 54 | 55 | # Symbols 56 | comment.gsub!(/(\s+:\w+)/, '\1'.red) 57 | 58 | # Constants 59 | comment.gsub!(/(([A-Z]\w+(::)?)+)/) do 60 | if @generator.constant?($1.strip) 61 | $1.split('::').map { |part| part.cyan }.join('::') 62 | else 63 | $1 64 | end 65 | end 66 | 67 | comment 68 | end 69 | 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/tomdoc/reporters/html.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | module Reporters 3 | class HTML < Base 4 | 5 | #def highlight(text) 6 | # pygments(text, '-l', 'ruby', '-f', 'html') 7 | #end 8 | 9 | def write_scope_header(scope, prefix) 10 | #write "

#{scope.name}

" 11 | end 12 | 13 | def before_all_methods(scope, prefix) 14 | write '' 19 | end 20 | 21 | def write_method(method, prefix = '') 22 | if method.args.any? 23 | args = '(' + method.args.join(', ') + ')' 24 | end 25 | out = '
  • ' 26 | out << "#{prefix}#{method.to_s}#{args}" 27 | 28 | out << '
    '
    29 |         out << method.tomdoc.tomdoc
    30 |         out << '
    ' 31 | 32 | out << '
  • ' 33 | write out 34 | end 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/tomdoc/scope.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | # A Scope is a Module or Class. 3 | # It may contain other scopes. 4 | class Scope 5 | include Enumerable 6 | 7 | attr_accessor :name, :comment, :instance_methods, :class_methods 8 | attr_accessor :scopes 9 | 10 | def initialize(name, comment = '', instance_methods = [], class_methods = []) 11 | @name = name 12 | @comment = comment 13 | @instance_methods = instance_methods 14 | @class_methods = class_methods 15 | @scopes = {} 16 | end 17 | 18 | def tomdoc 19 | @tomdoc ||= TomDoc.new(@comment) 20 | end 21 | 22 | def [](scope) 23 | @scopes[scope] 24 | end 25 | 26 | def keys 27 | @scopes.keys 28 | end 29 | 30 | def each(&block) 31 | @scopes.each(&block) 32 | end 33 | 34 | def to_s 35 | inspect 36 | end 37 | 38 | def inspect 39 | scopes = @scopes.keys.join(', ') 40 | imethods = @instance_methods.inspect 41 | cmethods = @class_methods.inspect 42 | 43 | "<#{name} scopes:[#{scopes}] :#{cmethods}: ##{imethods}#>" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/tomdoc/source_parser.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | class SourceParser 3 | # Converts Ruby code into a data structure. 4 | # 5 | # text - A String of Ruby code. 6 | # 7 | # Returns a Hash with each key a namespace and each value another 8 | # Hash or a TomDoc::Scope. 9 | def self.parse(text) 10 | new.parse(text) 11 | end 12 | 13 | attr_accessor :parser, :scopes, :options 14 | 15 | # Each instance of SourceParser accumulates scopes with each 16 | # parse, making it easy to parse an entire project in chunks but 17 | # more difficult to parse disparate files in one go. Create 18 | # separate instances for separate global scopes. 19 | # 20 | # Returns an instance of TomDoc::SourceParser 21 | def initialize(options = {}) 22 | @options = {} 23 | @parser = RubyParser.new 24 | @scopes = {} 25 | end 26 | 27 | # Resets the state of the parser to a pristine one. Maintains options. 28 | # 29 | # Returns nothing. 30 | def reset 31 | initialize(@options) 32 | end 33 | 34 | # Converts Ruby code into a data structure. Note that at the 35 | # instance level scopes accumulate, which makes it easy to parse 36 | # multiple files in a single project but harder to parse files 37 | # that have no connection. 38 | # 39 | # text - A String of Ruby code. 40 | # 41 | # Examples 42 | # @parser = TomDoc::SourceParser.new 43 | # files.each do |file| 44 | # @parser.parse(File.read(file)) 45 | # end 46 | # pp @parser.scopes 47 | # 48 | # Returns a Hash with each key a namespace and each value another 49 | # Hash or a TomDoc::Scope. 50 | def parse(text) 51 | process(tokenize(sexp(text))) 52 | @scopes 53 | end 54 | 55 | # Converts Ruby sourcecode into an AST. 56 | # 57 | # text - A String of Ruby source. 58 | # 59 | # Returns a Sexp representing the AST. 60 | def sexp(text) 61 | @parser.parse(text) 62 | end 63 | 64 | # Converts a tokenized Array of classes, modules, and methods into 65 | # Scopes and Methods, adding them to the @scopes instance variable 66 | # as it works. 67 | # 68 | # ast - Tokenized Array produced by calling `tokenize`. 69 | # scope - An optional Scope object for nested classes or modules. 70 | # 71 | # Returns nothing. 72 | def process(ast, scope = nil) 73 | case Array(ast)[0] 74 | when :module, :class 75 | name = ast[1] 76 | new_scope = Scope.new(name, ast[2]) 77 | 78 | if scope 79 | scope.scopes[name] = new_scope 80 | elsif @scopes[name] 81 | new_scope = @scopes[name] 82 | else 83 | @scopes[name] = new_scope 84 | end 85 | 86 | process(ast[3], new_scope) 87 | when :imethod 88 | ast.shift 89 | if !scope 90 | scope = (@scopes[:main] ||= Scope.new(:main)) 91 | end 92 | scope.instance_methods << Method.new(*ast) 93 | when :cmethod 94 | ast.shift 95 | if !scope 96 | scope = (@scopes[:main] ||= Scope.new(:main)) 97 | end 98 | scope.class_methods << Method.new(*ast) 99 | when Array 100 | ast.map { |a| process(a, scope) } 101 | end 102 | end 103 | 104 | # Converts a Ruby AST-style Sexp into an Array of more useful tokens. 105 | # 106 | # node - A Ruby AST Sexp or Array 107 | # 108 | # Examples 109 | # 110 | # [:module, :Math, "", 111 | # [:class, :Multiplexer, "# Class Comment", 112 | # [:cmethod, 113 | # :multiplex, "# Class Method Comment", [:text]], 114 | # [:imethod, 115 | # :multiplex, "# Instance Method Comment", [:text, :count]]]] 116 | # 117 | # # In others words: 118 | # # [ :type, :name, :comment, other ] 119 | # 120 | # Returns an Array in the above format. 121 | def tokenize(node) 122 | case Array(node)[0] 123 | when :module 124 | name = node[1] 125 | [ :module, name, node.comments, tokenize(node[2..-1]) ] 126 | when :class 127 | name = node[1] 128 | [ :class, name, node.comments, tokenize(node[3..-1]) ] 129 | when :defn 130 | name = node[1] 131 | args = args_for_node(node[2]) 132 | [ :imethod, name, node.comments, args ] 133 | when :defs 134 | name = node[2] 135 | args = args_for_node(node[3]) 136 | [ :cmethod, name, node.comments, args ] 137 | when :block 138 | tokenize(node[1..-1]) 139 | when :scope 140 | tokenize(node[1]) 141 | when Array 142 | node.map { |n| tokenize(n) }.compact 143 | end 144 | end 145 | 146 | # Given a method sexp, returns an array of the args. 147 | def args_for_node(node) 148 | Array(node)[1..-1].select { |arg| arg.is_a? Symbol } 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /lib/tomdoc/tomdoc.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | 3 | # TomDoc class needs Arg class. 4 | if RUBY_VERSION > '1.9' 5 | require_relative 'arg' 6 | else 7 | require 'tomdoc/arg' 8 | end 9 | 10 | class InvalidTomDoc < RuntimeError 11 | # Create new InvalidTomDoc object. 12 | # 13 | # doc - document string 14 | # 15 | def initialize(doc) 16 | @doc = doc 17 | end 18 | 19 | # Provide access to document string. 20 | # 21 | # Returns String. 22 | def message 23 | @doc 24 | end 25 | 26 | # Provide access to document string. 27 | # 28 | # Returns String. 29 | def to_s 30 | @doc 31 | end 32 | end 33 | 34 | # 35 | # TODO: Instead of having TomDoc::TomDoc, lets consider renaming this 36 | # class TomDoc::Comment or something. 37 | # 38 | class TomDoc 39 | attr_accessor :raw 40 | 41 | # Public: Initialize a TomDoc object. 42 | # 43 | # text - The raw text of a method or class/module comment. 44 | # 45 | # Returns new TomDoc instance. 46 | def initialize(text, options={}) 47 | @raw = text.to_s.strip 48 | 49 | @arguments = [] 50 | @examples = [] 51 | @returns = [] 52 | @raises = [] 53 | @signatures = [] 54 | @signature_fields = [] 55 | 56 | parse unless @raw.empty? 57 | end 58 | 59 | def to_s 60 | @raw 61 | end 62 | 63 | # Validate given comment text. 64 | # 65 | # Returns true if comment is valid, otherwise false. 66 | def self.valid?(text) 67 | new(text).valid? 68 | end 69 | 70 | # Validate raw comment. 71 | # 72 | # Returns true if comment is valid, otherwise false. 73 | def valid? 74 | return false if !raw.include?('Returns') 75 | return false if sections.size < 2 76 | true 77 | end 78 | 79 | # Validate raw comment. 80 | # 81 | # Returns true if comment is valid. 82 | # Raises InvalidTomDoc if comment is not valid. 83 | def validate 84 | if !raw.include?('Returns') 85 | raise InvalidTomDoc.new("No `Returns' statement.") 86 | end 87 | 88 | if sections.size < 2 89 | raise InvalidTomDoc.new("No description section found.") 90 | end 91 | 92 | true 93 | end 94 | 95 | # The raw comment text cleaned-up and ready for section parsing. 96 | # 97 | # Returns cleaned-up comment String. 98 | def tomdoc 99 | lines = raw.split("\n") 100 | 101 | # remove remark symbol 102 | if lines.all?{ |line| /^\s*#/ =~ line } 103 | lines = lines.map do |line| 104 | line =~ /^(\s*#)/ ? line.sub($1, '') : nil 105 | end 106 | end 107 | 108 | # for some reason the first line is coming in without indention 109 | # regardless, so we temporary remove it 110 | first = lines.shift 111 | 112 | lines = deindent(lines) 113 | 114 | # put first line back 115 | unless first.nil? 116 | lines.unshift(first.sub(/^\s*/,'')) 117 | end 118 | 119 | lines.compact.join("\n") 120 | end 121 | 122 | # List of comment sections. These are divided simply on "\n\n". 123 | # 124 | # Returns Array of comment sections. 125 | def sections 126 | parsed { 127 | @sections 128 | } 129 | end 130 | 131 | # Description of method or class/module. 132 | # 133 | # Returns description String. 134 | def description 135 | parsed { 136 | @description 137 | } 138 | end 139 | 140 | # Description of method or class/module. 141 | # 142 | # Returns description String. 143 | def arguments 144 | parsed { 145 | @arguments 146 | } 147 | end 148 | alias args arguments 149 | 150 | # List of use examples of a method or class/module. 151 | # 152 | # Returns String of examples. 153 | def examples 154 | parsed { 155 | @examples 156 | } 157 | end 158 | 159 | # Description of a methods yield procedure. 160 | # 161 | # Returns String decription of yield procedure. 162 | def yields 163 | parsed { 164 | @yields 165 | } 166 | end 167 | 168 | # The list of retrun values a method can return. 169 | # 170 | # Returns Array of method return descriptions. 171 | def returns 172 | parsed { 173 | @returns 174 | } 175 | end 176 | 177 | # A list of errors a method might raise. 178 | # 179 | # Returns Array of method raises descriptions. 180 | def raises 181 | parsed { 182 | @raises 183 | } 184 | end 185 | 186 | # A list of alternate method signatures. 187 | # 188 | # Returns Array of signatures. 189 | def signatures 190 | parsed { 191 | @signatures 192 | } 193 | end 194 | 195 | # A list of signature fields. 196 | # 197 | # Returns Array of field definitions. 198 | def signature_fields 199 | parsed { 200 | @signature_fields 201 | } 202 | end 203 | 204 | # Check if method is public. 205 | # 206 | # Returns true if method is public. 207 | def public? 208 | parsed { 209 | @status == 'Public' 210 | } 211 | end 212 | 213 | # Check if method is internal. 214 | # 215 | # Returns true if method is internal. 216 | def internal? 217 | parsed { 218 | @status == 'Internal' 219 | } 220 | end 221 | 222 | # Check if method is deprecated. 223 | # 224 | # Returns true if method is deprecated. 225 | def deprecated? 226 | parsed { 227 | @status == 'Deprecated' 228 | } 229 | end 230 | 231 | private 232 | 233 | def deindent(lines) 234 | # remove indention 235 | spaces = lines.map do |line| 236 | next if line.strip.empty? 237 | md = /^(\s*)/.match(line) 238 | md ? md[1].size : nil 239 | end.compact 240 | 241 | space = spaces.min || 0 242 | lines.map do |line| 243 | if line.empty? 244 | line.strip 245 | else 246 | line[space..-1] 247 | end 248 | end 249 | end 250 | 251 | # Has the comment been parsed yet? 252 | def parsed(&block) 253 | parse unless @parsed 254 | block.call 255 | end 256 | 257 | # Internal: Parse the Tomdoc formatted comment. 258 | # 259 | # Returns true if there was a comment to parse. 260 | def parse 261 | @parsed = true 262 | 263 | @sections = tomdoc.split("\n\n") 264 | sections = @sections.dup 265 | 266 | return false if sections.empty? 267 | 268 | parse_description(sections.shift) 269 | 270 | if sections.first && sections.first =~ /^\w+\s+\-/m 271 | parse_arguments(sections.shift) 272 | end 273 | 274 | current = sections.shift 275 | while current 276 | case current 277 | when /^Examples/ 278 | parse_examples(current, sections) 279 | when /^Yields/ 280 | parse_yields(current) 281 | when /^(Returns|Raises)/ 282 | parse_returns(current) 283 | when /^Signature/ 284 | parse_signature(current, sections) 285 | end 286 | current = sections.shift 287 | end 288 | 289 | return @parsed 290 | end 291 | 292 | # Parse description. 293 | # 294 | # section - String containig description. 295 | # 296 | # Returns nothing. 297 | def parse_description(section) 298 | if md = /^([A-Z]\w+\:)/.match(section) 299 | @status = md[1].chomp(':') 300 | @description = md.post_match.strip 301 | else 302 | @description = section.strip 303 | end 304 | end 305 | 306 | # Parse arguments section. Arguments occur subsequent to 307 | # the description. 308 | # 309 | # section - String contaning agument definitions. 310 | # 311 | # Returns nothing. 312 | def parse_arguments(section) 313 | args = [] 314 | last_indent = nil 315 | 316 | section.split("\n").each do |line| 317 | next if line.strip.empty? 318 | indent = line.scan(/^\s*/)[0].to_s.size 319 | 320 | if last_indent && indent > last_indent 321 | args.last.description += line.squeeze(" ") 322 | else 323 | param, desc = line.split(" - ") 324 | args << Arg.new(param.strip, desc.strip) if param && desc 325 | end 326 | 327 | last_indent = indent 328 | end 329 | 330 | @arguments = args 331 | end 332 | 333 | # Parse examples. 334 | # 335 | # section - String starting with `Examples`. 336 | # sections - All sections subsequent to section. 337 | # 338 | # Returns nothing. 339 | def parse_examples(section, sections) 340 | examples = [] 341 | 342 | section = section.sub('Examples', '').strip 343 | 344 | examples << section unless section.empty? 345 | while sections.first && sections.first !~ /^\S/ 346 | lines = sections.shift.split("\n") 347 | examples << deindent(lines).join("\n") 348 | end 349 | 350 | @examples = examples 351 | end 352 | 353 | # Parse yields section. 354 | # 355 | # section - String contaning Yields line. 356 | # 357 | # Returns nothing. 358 | def parse_yields(section) 359 | @yields = section.strip 360 | end 361 | 362 | # Parse returns section. 363 | # 364 | # section - String contaning Returns and/or Raises lines. 365 | # 366 | # Returns nothing. 367 | def parse_returns(section) 368 | returns, raises, current = [], [], [] 369 | 370 | lines = section.split("\n") 371 | lines.each do |line| 372 | case line 373 | when /^Returns/ 374 | returns << line 375 | current = returns 376 | when /^Raises/ 377 | raises << line 378 | current = raises 379 | when /^\s+/ 380 | current.last << line.squeeze(' ') 381 | else 382 | current << line # TODO: What to do with non-compliant line? 383 | end 384 | end 385 | 386 | @returns, @raises = returns, raises 387 | end 388 | 389 | # Parse signature section. 390 | # 391 | # section - String starting with `Signature`. 392 | # sections - All sections subsequent to section. 393 | # 394 | # Returns nothing. 395 | def parse_signature(section, sections=[]) 396 | signatures = [] 397 | 398 | section = section.sub('Signature', '').strip 399 | 400 | signatures << section unless section.empty? 401 | 402 | while sections.first && sections.first !~ /^\S/ 403 | sigs = sections.shift 404 | sigs.split("\n").each do |s| 405 | signatures << s.strip 406 | end 407 | end 408 | 409 | @signatures = signatures 410 | 411 | if sections.first && sections.first =~ /^\w+\s*\-/m 412 | parse_signature_fields(sections.shift) 413 | end 414 | end 415 | 416 | # Subsequent to Signature section there can be field 417 | # definitions. 418 | # 419 | # section - String subsequent to signatures. 420 | # 421 | # Returns nothing. 422 | def parse_signature_fields(section) 423 | args = [] 424 | last_indent = nil 425 | 426 | section.split("\n").each do |line| 427 | next if line.strip.empty? 428 | indent = line.scan(/^\s*/)[0].to_s.size 429 | 430 | if last_indent && indent > last_indent 431 | args.last.description += line.squeeze(" ") 432 | else 433 | param, desc = line.split(" - ") 434 | args << Arg.new(param.strip, desc.strip) if param && desc 435 | end 436 | 437 | last_indent = indent 438 | end 439 | 440 | @signature_fields = args 441 | end 442 | 443 | end 444 | end 445 | -------------------------------------------------------------------------------- /lib/tomdoc/version.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | VERSION = '0.2.5' 3 | end 4 | -------------------------------------------------------------------------------- /man/tomdoc.5: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.5 2 | .\" http://github.com/rtomayko/ronn/ 3 | . 4 | .TH "TOMDOC" "5" "May 2010" "MOJOMBO" "TomDoc Manual" 5 | . 6 | .SH "NAME" 7 | \fBtomdoc\fR \-\- TomDoc for Ruby \- Version 0.9.0 8 | . 9 | .SH "Purpose" 10 | TomDoc is a code documentation specification that helps you write precise 11 | documentation that is nice to read in plain text, yet structured enough to be 12 | automatically extracted and processed by a machine. 13 | . 14 | .P 15 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", 16 | "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be 17 | interpreted as described in RFC 2119. 18 | . 19 | .SH "Class/Module Documentation" 20 | TomDoc for classes and modules consists of a block of single comment markers 21 | (#) that appear directly above the class/module definition. Lines SHOULD be 22 | wrapped at 80 characters. Lines that contain text MUST be separated from the 23 | comment marker by a single space. Lines that do not contain text SHOULD 24 | consist of just a comment marker (no trailing spaces). 25 | . 26 | .P 27 | Code examples SHOULD be indented two spaces (three spaces from the comment 28 | marker). 29 | . 30 | .IP "" 4 31 | . 32 | .nf 33 | 34 | # Various methods useful for performing mathematical operations. All 35 | # methods are module methods and should be called on the Math module. 36 | # For example: 37 | # 38 | # Math.square_root(9) 39 | # # => 3 40 | # 41 | module Math 42 | ... 43 | end 44 | . 45 | .fi 46 | . 47 | .IP "" 0 48 | . 49 | .SH "Method Documentation" 50 | A quick example will serve to best illustrate the TomDoc method documentation 51 | format: 52 | . 53 | .IP "" 4 54 | . 55 | .nf 56 | 57 | # Duplicate some text an abitrary number of times. 58 | # 59 | # text \- The String to be duplicated. 60 | # count \- The Integer number of times to duplicate the text. 61 | # 62 | # Examples 63 | # 64 | # multiplex('Tom', 4) 65 | # # => 'TomTomTomTom' 66 | # 67 | # Returns the duplicated String. 68 | def multiplex(text, count) 69 | text * count 70 | end 71 | . 72 | .fi 73 | . 74 | .IP "" 0 75 | . 76 | .P 77 | TomDoc for a specific method consists of a block of single comment markers (#) 78 | that appears directly above the method. There MUST NOT be a blank line between 79 | the comment block and the method definition. A TomDoc method block consists of 80 | a description section (required), an arguments section (required if the method 81 | takes any arguments), an examples section (optional), and a returns section 82 | (required). Lines that contain text MUST be separated from the comment 83 | marker by a single space. Lines that do not contain text SHOULD consist of 84 | just a comment marker (no trailing spaces). 85 | . 86 | .SS "The Description Section" 87 | The description section SHOULD be in plain sentences. Each sentence SHOULD end 88 | with a period. Good descriptions explain what the code does at a high level. 89 | Make sure to explain any unexpected behavior that the method may have, or any 90 | pitfalls that the user may experience. Lines SHOULD be wrapped at 80 91 | characters. 92 | . 93 | .P 94 | If a method's description begins with "Public:" then that method will be 95 | considered part of the project's public API. For example: 96 | . 97 | .IP "" 4 98 | . 99 | .nf 100 | 101 | # Public: Initialize a new Widget. 102 | . 103 | .fi 104 | . 105 | .IP "" 0 106 | . 107 | .P 108 | This annotation is designed to let developers know which methods are 109 | considered stable. You SHOULD use this to document the public API of your 110 | project. This information can then be used along with \fISemantic 111 | Versioning\fR to inform decisions on when major, minor, and 112 | patch versions should be incremented. 113 | . 114 | .P 115 | If a method's description begins with "Deprecated:" then that method will be 116 | considered as deprecated and users will know that it will be removed in a 117 | future version. 118 | . 119 | .SS "The Arguments Section" 120 | The arguments section consists of a list of arguments. Each list item MUST be 121 | comprised of the name of the argument, a dash, and an explanation of the 122 | argument in plain sentences. The expected type (or types) of each argument 123 | SHOULD be clearly indicated in the explanation. When you specify a type, use 124 | the proper classname of the type (for instance, use 'String' instead of 125 | 'string' to refer to a String type). The dashes following each argument name 126 | should be lined up in a single column. Lines SHOULD be wrapped at 80 columns. 127 | If an explanation is longer than that, additional lines MUST be indented at 128 | least two spaces but SHOULD be indented to match the indentation of the 129 | explanation. For example: 130 | . 131 | .IP "" 4 132 | . 133 | .nf 134 | 135 | # element \- The Symbol representation of the element. The Symbol should 136 | # contain only lowercase ASCII alpha characters. 137 | . 138 | .fi 139 | . 140 | .IP "" 0 141 | . 142 | .P 143 | All arguments are assumed to be required. If an argument is optional, you MUST 144 | specify the default value: 145 | . 146 | .IP "" 4 147 | . 148 | .nf 149 | 150 | # host \- The String hostname to bind (default: '0.0.0.0'). 151 | . 152 | .fi 153 | . 154 | .IP "" 0 155 | . 156 | .P 157 | For hash arguments, you SHOULD enumerate each valid option in a way similar 158 | to how normal arguments are defined: 159 | . 160 | .IP "" 4 161 | . 162 | .nf 163 | 164 | # options \- The Hash options used to refine the selection (default: {}): 165 | # :color \- The String color to restrict by (optional). 166 | # :weight \- The Float weight to restrict by. The weight should 167 | # be specified in grams (optional). 168 | . 169 | .fi 170 | . 171 | .IP "" 0 172 | . 173 | .SS "The Examples Section" 174 | The examples section MUST start with the word "Examples" on a line by 175 | itself. The next line SHOULD be blank. The following lines SHOULD be indented 176 | by two spaces (three spaces from the initial comment marker) and contain code 177 | that shows off how to call the method and (optional) examples of what it 178 | returns. Everything under the "Examples" line should be considered code, so 179 | make sure you comment out lines that show return values. Separate examples 180 | should be separated by a blank line. For example: 181 | . 182 | .IP "" 4 183 | . 184 | .nf 185 | 186 | # Examples 187 | # 188 | # multiplex('x', 4) 189 | # # => 'xxxx' 190 | # 191 | # multiplex('apple', 2) 192 | # # => 'appleapple' 193 | . 194 | .fi 195 | . 196 | .IP "" 0 197 | . 198 | .SS "The Returns Section" 199 | The returns section should explain in plain sentences what is returned from 200 | the method. The line MUST begin with "Returns". If only a single thing is 201 | returned, state the nature and type of the value. For example: 202 | . 203 | .IP "" 4 204 | . 205 | .nf 206 | 207 | # Returns the duplicated String. 208 | . 209 | .fi 210 | . 211 | .IP "" 0 212 | . 213 | .P 214 | If several different types may be returned, list all of them. For example: 215 | . 216 | .IP "" 4 217 | . 218 | .nf 219 | 220 | # Returns the given element Symbol or nil if none was found. 221 | . 222 | .fi 223 | . 224 | .IP "" 0 225 | . 226 | .P 227 | If the return value of the method is not intended to be used, then you should 228 | simply state: 229 | . 230 | .IP "" 4 231 | . 232 | .nf 233 | 234 | # Returns nothing. 235 | . 236 | .fi 237 | . 238 | .IP "" 0 239 | . 240 | .P 241 | If the method raises exceptions that the caller may be interested in, add 242 | additional lines that explain each exception and under what conditions it may 243 | be encountered. The lines MUST begin with "Raises". For example: 244 | . 245 | .IP "" 4 246 | . 247 | .nf 248 | 249 | # Returns nothing. 250 | # Raises Errno::ENOENT if the file cannot be found. 251 | # Raises Errno::EACCES if the file cannot be accessed. 252 | . 253 | .fi 254 | . 255 | .IP "" 0 256 | . 257 | .P 258 | Lines SHOULD be wrapped at 80 columns. Wrapped lines MUST be indented under 259 | the above line by at least two spaces. For example: 260 | . 261 | .IP "" 4 262 | . 263 | .nf 264 | 265 | # Returns the atomic mass of the element as a Float. The value is in 266 | # unified atomic mass units. 267 | . 268 | .fi 269 | . 270 | .IP "" 0 271 | . 272 | .SH "Special Considerations" 273 | . 274 | .SS "Attributes" 275 | Ruby's built in \fBattr_reader\fR, \fBattr_writer\fR, and \fBattr_accessor\fR require a 276 | bit more consideration. With TomDoc you SHOULD NOT use \fBattr_access\fR since it 277 | represents two methods with different signatures. Restricting yourself in this 278 | way also makes you think more carefully about the read vs. write behavior and 279 | whether each should be part of the Public API. 280 | . 281 | .P 282 | Here is an example TomDoc for \fBattr_reader\fR. 283 | . 284 | .IP "" 4 285 | . 286 | .nf 287 | 288 | # Public: Get the user's name. 289 | # 290 | # Returns the String name of the user. 291 | attr_reader :name 292 | . 293 | .fi 294 | . 295 | .IP "" 0 296 | . 297 | .P 298 | Here is an example TomDoc for \fBattr_writer\fR. The parameter name should be the 299 | same as the attribute name. 300 | . 301 | .IP "" 4 302 | . 303 | .nf 304 | 305 | # Set the user's name. 306 | # 307 | # name \- The String name of the user. 308 | # 309 | # Returns nothing. 310 | attr_writer :name 311 | . 312 | .fi 313 | . 314 | .IP "" 0 315 | . 316 | .P 317 | While this approach certainly takes up more space than listing dozens of 318 | attributes on a single line, it allows for individual documentation of each 319 | attribute. Attributes are an extremely important part of a class and should be 320 | treated with the same care as any other methods. 321 | -------------------------------------------------------------------------------- /man/tomdoc.5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tomdoc(5) -- TomDoc for Ruby - Version 0.9.0 7 | 53 | 54 | 55 |
    56 | 57 |

    tomdoc(5)

    58 | 59 |
      60 |
    1. tomdoc(5)
    2. 61 |
    3. TomDoc Manual
    4. 62 |
    5. tomdoc(5)
    6. 63 |
    64 | 65 |

    NAME

    66 |

    tomdoc -- TomDoc for Ruby - Version 0.9.0

    67 | 68 |

    Purpose

    69 | 70 |

    TomDoc is a code documentation specification that helps you write precise 71 | documentation that is nice to read in plain text, yet structured enough to be 72 | automatically extracted and processed by a machine.

    73 | 74 |

    The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", 75 | "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be 76 | interpreted as described in RFC 2119.

    77 | 78 |

    Class/Module Documentation

    79 | 80 |

    TomDoc for classes and modules consists of a block of single comment markers 81 | (#) that appear directly above the class/module definition. Lines SHOULD be 82 | wrapped at 80 characters. Lines that contain text MUST be separated from the 83 | comment marker by a single space. Lines that do not contain text SHOULD 84 | consist of just a comment marker (no trailing spaces).

    85 | 86 |

    Code examples SHOULD be indented two spaces (three spaces from the comment 87 | marker).

    88 | 89 |
    # Various methods useful for performing mathematical operations. All
     90 | # methods are module methods and should be called on the Math module.
     91 | # For example:
     92 | #
     93 | #   Math.square_root(9)
     94 | #   # => 3
     95 | #
     96 | module Math
     97 |   ...
     98 | end
     99 | 
    100 | 101 |

    Method Documentation

    102 | 103 |

    A quick example will serve to best illustrate the TomDoc method documentation 104 | format:

    105 | 106 |
    # Duplicate some text an abitrary number of times.
    107 | #
    108 | # text  - The String to be duplicated.
    109 | # count - The Integer number of times to duplicate the text.
    110 | #
    111 | # Examples
    112 | #
    113 | #   multiplex('Tom', 4)
    114 | #   # => 'TomTomTomTom'
    115 | #
    116 | # Returns the duplicated String.
    117 | def multiplex(text, count)
    118 |   text * count
    119 | end
    120 | 
    121 | 122 |

    TomDoc for a specific method consists of a block of single comment markers (#) 123 | that appears directly above the method. There MUST NOT be a blank line between 124 | the comment block and the method definition. A TomDoc method block consists of 125 | a description section (required), an arguments section (required if the method 126 | takes any arguments), an examples section (optional), and a returns section 127 | (required). Lines that contain text MUST be separated from the comment 128 | marker by a single space. Lines that do not contain text SHOULD consist of 129 | just a comment marker (no trailing spaces).

    130 | 131 |

    The Description Section

    132 | 133 |

    The description section SHOULD be in plain sentences. Each sentence SHOULD end 134 | with a period. Good descriptions explain what the code does at a high level. 135 | Make sure to explain any unexpected behavior that the method may have, or any 136 | pitfalls that the user may experience. Lines SHOULD be wrapped at 80 137 | characters.

    138 | 139 |

    If a method's description begins with "Public:" then that method will be 140 | considered part of the project's public API. For example:

    141 | 142 |
    # Public: Initialize a new Widget.
    143 | 
    144 | 145 |

    This annotation is designed to let developers know which methods are 146 | considered stable. You SHOULD use this to document the public API of your 147 | project. This information can then be used along with Semantic 148 | Versioning to inform decisions on when major, minor, and 149 | patch versions should be incremented.

    150 | 151 |

    If a method's description begins with "Deprecated:" then that method will be 152 | considered as deprecated and users will know that it will be removed in a 153 | future version.

    154 | 155 |

    The Arguments Section

    156 | 157 |

    The arguments section consists of a list of arguments. Each list item MUST be 158 | comprised of the name of the argument, a dash, and an explanation of the 159 | argument in plain sentences. The expected type (or types) of each argument 160 | SHOULD be clearly indicated in the explanation. When you specify a type, use 161 | the proper classname of the type (for instance, use 'String' instead of 162 | 'string' to refer to a String type). The dashes following each argument name 163 | should be lined up in a single column. Lines SHOULD be wrapped at 80 columns. 164 | If an explanation is longer than that, additional lines MUST be indented at 165 | least two spaces but SHOULD be indented to match the indentation of the 166 | explanation. For example:

    167 | 168 |
    # element - The Symbol representation of the element. The Symbol should
    169 | #           contain only lowercase ASCII alpha characters.
    170 | 
    171 | 172 |

    All arguments are assumed to be required. If an argument is optional, you MUST 173 | specify the default value:

    174 | 175 |
    # host - The String hostname to bind (default: '0.0.0.0').
    176 | 
    177 | 178 |

    For hash arguments, you SHOULD enumerate each valid option in a way similar 179 | to how normal arguments are defined:

    180 | 181 |
    # options - The Hash options used to refine the selection (default: {}):
    182 | #           :color  - The String color to restrict by (optional).
    183 | #           :weight - The Float weight to restrict by. The weight should
    184 | #                     be specified in grams (optional).
    185 | 
    186 | 187 |

    The Examples Section

    188 | 189 |

    The examples section MUST start with the word "Examples" on a line by 190 | itself. The next line SHOULD be blank. The following lines SHOULD be indented 191 | by two spaces (three spaces from the initial comment marker) and contain code 192 | that shows off how to call the method and (optional) examples of what it 193 | returns. Everything under the "Examples" line should be considered code, so 194 | make sure you comment out lines that show return values. Separate examples 195 | should be separated by a blank line. For example:

    196 | 197 |
    # Examples
    198 | #
    199 | #   multiplex('x', 4)
    200 | #   # => 'xxxx'
    201 | #
    202 | #   multiplex('apple', 2)
    203 | #   # => 'appleapple'
    204 | 
    205 | 206 |

    The Returns Section

    207 | 208 |

    The returns section should explain in plain sentences what is returned from 209 | the method. The line MUST begin with "Returns". If only a single thing is 210 | returned, state the nature and type of the value. For example:

    211 | 212 |
    # Returns the duplicated String.
    213 | 
    214 | 215 |

    If several different types may be returned, list all of them. For example:

    216 | 217 |
    # Returns the given element Symbol or nil if none was found.
    218 | 
    219 | 220 |

    If the return value of the method is not intended to be used, then you should 221 | simply state:

    222 | 223 |
    # Returns nothing.
    224 | 
    225 | 226 |

    If the method raises exceptions that the caller may be interested in, add 227 | additional lines that explain each exception and under what conditions it may 228 | be encountered. The lines MUST begin with "Raises". For example:

    229 | 230 |
    # Returns nothing.
    231 | # Raises Errno::ENOENT if the file cannot be found.
    232 | # Raises Errno::EACCES if the file cannot be accessed.
    233 | 
    234 | 235 |

    Lines SHOULD be wrapped at 80 columns. Wrapped lines MUST be indented under 236 | the above line by at least two spaces. For example:

    237 | 238 |
    # Returns the atomic mass of the element as a Float. The value is in
    239 | #   unified atomic mass units.
    240 | 
    241 | 242 |

    Special Considerations

    243 | 244 |

    Attributes

    245 | 246 |

    Ruby's built in attr_reader, attr_writer, and attr_accessor require a 247 | bit more consideration. With TomDoc you SHOULD NOT use attr_access since it 248 | represents two methods with different signatures. Restricting yourself in this 249 | way also makes you think more carefully about the read vs. write behavior and 250 | whether each should be part of the Public API.

    251 | 252 |

    Here is an example TomDoc for attr_reader.

    253 | 254 |
    # Public: Get the user's name.
    255 | #
    256 | # Returns the String name of the user.
    257 | attr_reader :name
    258 | 
    259 | 260 |

    Here is an example TomDoc for attr_writer. The parameter name should be the 261 | same as the attribute name.

    262 | 263 |
    # Set the user's name.
    264 | #
    265 | # name - The String name of the user.
    266 | #
    267 | # Returns nothing.
    268 | attr_writer :name
    269 | 
    270 | 271 |

    While this approach certainly takes up more space than listing dozens of 272 | attributes on a single line, it allows for individual documentation of each 273 | attribute. Attributes are an extremely important part of a class and should be 274 | treated with the same care as any other methods.

    275 | 276 | 277 |
      278 |
    1. MOJOMBO
    2. 279 |
    3. May 2010
    4. 280 |
    5. tomdoc(5)
    6. 281 |
    282 | 283 |
    284 | 285 | 286 | -------------------------------------------------------------------------------- /man/tomdoc.5.ronn: -------------------------------------------------------------------------------- 1 | ../tomdoc.md -------------------------------------------------------------------------------- /test/console_reporter_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/helper' 2 | 3 | class ConsoleReporterTest < TomDoc::Test 4 | def setup 5 | @text = TomDoc::Generator.new(:report => TomDoc::Reporters::Console).generate(fixture(:simple)) 6 | end 7 | 8 | test "works" do 9 | assert_equal < 's1.example.com' 44 | # percent_used = get_partition_usage(part) # => 17.23 45 | # chimney.set_partition_usage(host, part, percent_used) 46 | # end 47 | # 48 | # Usage 49 | # ----- 50 | # 51 | # Make sure you require this sucker. 52 | # 53 | # require 'chimney' 54 | # 55 | # Chimney must be initialized with the host:port of the routing Redis server. 56 | # 57 | # chimney = Chimney.new('router.example.com:21201') 58 | # 59 | # Looking up a route for a user is simple. This command simply finds the host 60 | # upon which the user is stored. If the router Redis is unreachable, Chimney 61 | # will check its internal cache. If that is a miss, it will try to reconnect to 62 | # the router. If that fails, it will fallback on making calls to Smoke and 63 | # checking each storage server for the user. Subsequent lookups will then be 64 | # able to find the route in the cache. This mechanism should ensure high 65 | # tolerance to failures of the routing server. 66 | # 67 | # chimney.get_user_route('mojombo') 68 | # # => 'domU-12-31-38-01-C8-F1.compute-1.internal' 69 | # 70 | # Setting a route for a new user is also a simple call. This command will first 71 | # refresh the cached list of available storage hosts, then figure out which one 72 | # of them is least loaded. This host will be set as the route for the user and 73 | # returned. If the user already exists in the routing table, the host is 74 | # returned and the routing table is unaffected. 75 | # 76 | # chimney.set_user_route('franko') 77 | # # => domU-12-31-38-01-C8-F1.compute-1.internal 78 | # 79 | # If you need to change the name of the user, but keep the host the same: 80 | # 81 | # chimney.rename_user_route('oldname', 'newname') 82 | # 83 | # If you need to remove a route for a user: 84 | # 85 | # chimney.delete_user_route('mojombo') 86 | # 87 | # If you need the absolute path to a user on disk (class or instance method): 88 | # 89 | # Chimney.shard_user_path('mojombo') 90 | # chimney.shard_user_path('mojombo') 91 | # # => "/data/repositories/2/a8/e2/95/mojombo" 92 | # 93 | # If you need the absolute path to a repo on disk (class or instance method): 94 | # 95 | # Chimney.shard_repo_path('mojombo', 'god') 96 | # chimney.shard_repo_path('mojombo', 'god') 97 | # # => "/data/repositories/2/a8/e2/95/mojombo/god.git" 98 | # 99 | # Getting and setting routes for gists is similar to that for users: 100 | # 101 | # chimney.get_gist_route('1234') 102 | # # => 'domU-12-31-38-01-C8-F1.compute-1.internal' 103 | # 104 | # chimney.set_gist_route('4e460bfd6c184058c7a3') 105 | # # => 'domU-12-31-38-01-C8-F1.compute-1.internal' 106 | # 107 | # If you need the absolute path to a gist on disk (class or instance method): 108 | # 109 | # Chimney.shard_gist_path('1234') 110 | # chimney.shard_gist_path('1234') 111 | # # => "/data/repositories/0/81/dc/9b/gist/1234.git" 112 | # 113 | # If you need the unix user that has access to the repository data (class or 114 | # instance method): 115 | # 116 | # Chimney.unix_user 117 | # chimney.unix_user 118 | # # => 'root' 119 | # 120 | # That's it! 121 | class Chimney 122 | SMOKE_HOSTS_FILE = '/tmp/smoke_hosts' 123 | REPO_DIR = ENV['REPO_ROOT'] || '/data/repositories' 124 | UNIX_USER = 'git' 125 | 126 | attr_accessor :host, :port 127 | attr_accessor :client, :hosts, :cache, :verbose, :logger 128 | 129 | # Instantiate a new Chimney object. 130 | # 131 | # server - The host:port of the routing redis instance. 132 | # logger - An optional Logger object. If none is given, Chimney 133 | # writes to /dev/null. 134 | # 135 | # Returns a configured Chimney instance. 136 | def initialize(server, logger = nil) 137 | self.cache = {} 138 | self.hosts = [] 139 | self.logger = logger || Logger.new('/dev/null') 140 | 141 | self.host = server.split(':').first 142 | self.port = server.split(':').last.to_i 143 | ensure_client_connection 144 | end 145 | 146 | # Add a storage server to the list. 147 | # 148 | # host - The String hostname to add. 149 | # 150 | # Returns the Array of String hostnames after the addition. 151 | def self.add_storage_server(host) 152 | if current_servers = self.client.get('gh.storage.servers') 153 | new_servers = [current_servers, host].join(',') 154 | else 155 | new_servers = host 156 | end 157 | self.client.set('gh.storage.servers', new_servers) 158 | new_servers.split(',') 159 | end 160 | 161 | # Remove a storage server from the list. 162 | # 163 | # host - The String hostname to remove. 164 | # 165 | # Returns the Array of String hostnames after the removal. 166 | # Raises Chimney::NoSuchStorageServer if the storage server is not currently 167 | # in the list. 168 | def remove_storage_server(host) 169 | if current_servers = self.client.get('gh.storage.servers') 170 | servers = current_servers.split(',') 171 | if servers.delete(host) 172 | self.client.set('gh.storage.servers', servers.join(',')) 173 | return servers 174 | else 175 | raise NoSuchStorageServer.new(host) 176 | end 177 | else 178 | raise NoSuchStorageServer.new(host) 179 | end 180 | end 181 | 182 | # The list of storage server hostnames. 183 | # 184 | # Returns an Array of String hostnames. 185 | def storage_servers 186 | self.client.get('gh.storage.servers').split(',') 187 | end 188 | 189 | # Checks if the storage server is currently online. 190 | # 191 | # host - The String hostname to check. 192 | # 193 | # Returns true if the server is online, false if not. 194 | def storage_server_online?(host) 195 | !self.client.exists("gh.storage.server.offline.#{host}") 196 | rescue Errno::ECONNREFUSED 197 | # If we can't connect to Redis, check to see if the BERTRPC 198 | # server is alive manually. 199 | begin 200 | smoke(host).alive? 201 | rescue BERTRPC::ReadTimeoutError 202 | false 203 | end 204 | end 205 | 206 | # Sets a storage server as being online. 207 | # 208 | # host - The String hostname to set. 209 | # 210 | # Returns nothing. 211 | def set_storage_server_online(host) 212 | self.client.delete("gh.storage.server.offline.#{host}") 213 | end 214 | 215 | # Sets a storage server as being offline. 216 | # 217 | # host - The String hostname to set. 218 | # duration - An optional number of seconds after which the 219 | # server will no longer be considered offline; with 220 | # no duration, servers are kept offline until marked 221 | # online manually. 222 | # 223 | # Returns true if the server was not previously offline, nil otherwise. 224 | def set_storage_server_offline(host, duration=nil) 225 | key = "gh.storage.server.offline.#{host}" 226 | if self.client.set_unless_exists(key, Time.now.to_i) 227 | self.client.expire(key, duration) if duration 228 | true 229 | end 230 | end 231 | 232 | # If a server is offline, tells us when we first noticed. 233 | # 234 | # host - The String hostname to check. 235 | # 236 | # Returns nothing if the storage server is online. 237 | # Returns an instance of Time representing the moment we set the 238 | # server as offline if it is offline. 239 | def self.storage_server_offline_since(host) 240 | if time = self.client.get("gh.storage.server.offline.#{host}") 241 | Time.at(time.to_i) 242 | end 243 | rescue Errno::ECONNREFUSED 244 | # If we can't connect to Redis and we're wondering when the 245 | # storage server went offline, return whatever. 246 | Time.now 247 | end 248 | 249 | # Maximum number of network failures that can occur with a file server 250 | # before it's marked offline. 251 | DISRUPTION_THRESHOLD = 10 252 | 253 | # The window of time, in seconds, under which no more than 254 | # DISRUPTION_THRESHOLD failures may occur. 255 | DISRUPTION_WINDOW = 5 256 | 257 | # Called when some kind of network disruption occurs when communicating 258 | # with a file server. When more than DISRUPTION_THRESHOLD failures are 259 | # reported within DISRUPTION_WINDOW seconds, the server is marked offline 260 | # for two minutes. 261 | # 262 | # The return value can be used to determine the action taken: 263 | # nil when the storage server is already marked offline. 264 | # > 0 when the number of disruptions is under the threshold. 265 | # -1 when the server has been marked offline due to too many disruptions. 266 | def storage_server_disruption(host) 267 | return if !self.storage_server_online?(host) 268 | key = "gh.storage.server.disrupt.#{host}" 269 | if counter_suffix = self.client.get(key) 270 | count = self.client.incr("#{key}.#{counter_suffix}") 271 | if count > DISRUPTION_THRESHOLD 272 | if self.set_storage_server_offline(host, 30) 273 | self.client.del(key, "#{key}.#{counter_suffix}") 274 | -1 275 | end 276 | else 277 | count 278 | end 279 | else 280 | if self.client.set_unless_exists(key, Time.now.to_f * 1000) 281 | self.client.expire(key, DISRUPTION_WINDOW) 282 | self.storage_server_disruption(host) 283 | else 284 | # we raced to set first and lost, wrap around and try again 285 | self.storage_server_disruption(host) 286 | end 287 | end 288 | end 289 | 290 | # Lookup a route for the given user. 291 | # 292 | # user - The String username. 293 | # 294 | # Returns the hostname of the storage server. 295 | def get_user_route(user) 296 | try_route(:user, user) 297 | end 298 | 299 | # Lookup a route for the given gist. 300 | # 301 | # gist - The String gist ID. 302 | # 303 | # Returns the hostname of the storage server. 304 | def get_gist_route(gist) 305 | try_route(:gist, gist) 306 | end 307 | 308 | # Find the least loaded storage server and set a route there for 309 | # the given +user+. If the user already exists, do nothing and 310 | # simply return the host that user is on. 311 | # 312 | # user - The String username. 313 | # 314 | # Returns the chosen hostname. 315 | def set_user_route(user) 316 | set_route(:user, user) 317 | end 318 | 319 | # Explicitly set the user route to the given host. 320 | # 321 | # user - The String username. 322 | # host - The String hostname. 323 | # 324 | # Returns the new String hostname. 325 | # Raises Chimney::NoSuchStorageServer if the storage server is not currently 326 | # in the list. 327 | def set_user_route!(user, host) 328 | unless self.storage_servers.include?(host) 329 | raise NoSuchStorageServer.new(host) 330 | end 331 | set_route(:user, user, host) 332 | end 333 | 334 | # Find the least loaded storage server and set a route there for 335 | # the given +gist+. If the gist already exists, do nothing and 336 | # simply return the host that gist is on. 337 | # 338 | # gist - The String gist ID. 339 | # 340 | # Returns the chosen hostname. 341 | def set_gist_route(gist) 342 | set_route(:gist, gist) 343 | end 344 | 345 | # Change the name of the given user without changing the associated host. 346 | # 347 | # old_user - The old user name. 348 | # new_user - The new user name. 349 | # 350 | # Returns the hostname on success, or nil if the old user was not found 351 | # or if the new user already exists. 352 | def rename_user_route(old_user, new_user) 353 | if (host = get_user_route(old_user)) && !get_user_route(new_user) 354 | delete_user_route(old_user) 355 | set_route(:user, new_user, host) 356 | else 357 | nil 358 | end 359 | end 360 | 361 | # Delete the route for the given user. 362 | # 363 | # user - The String username. 364 | # 365 | # Returns nothing. 366 | def delete_user_route(user) 367 | self.client.delete("gh.storage.user.#{user}") 368 | end 369 | 370 | # Delete the route for the given gist. 371 | # 372 | # gist - The String gist ID. 373 | # 374 | # Returns nothing. 375 | def delete_gist_route(gist) 376 | self.client.delete("gh.storage.gist.#{gist}") 377 | end 378 | 379 | # Set the partition usage for a given host. 380 | # 381 | # host - The String hostname. 382 | # partition - The single lowercase hex digit partition String. 383 | # usage - The percent of disk space used as a Float [0.0-100.0]. 384 | # 385 | # Returns nothing. 386 | def set_partition_usage(host, partition, usage) 387 | self.client.set("gh.storage.server.usage.percent.#{host}.#{partition}", usage.to_s) 388 | end 389 | 390 | # The list of partition usage percentages. 391 | # 392 | # host - The optional String hostname to restrict the response to. 393 | # 394 | # Returns an Array of [partition:String, percentage:Float]. 395 | def partition_usage(host = nil) 396 | pattern = "gh.storage.server.usage.percent." 397 | pattern += host ? "#{host}.*" : "*" 398 | self.client.keys(pattern).map do |x| 399 | [x, self.client.get(x).to_f] 400 | end 401 | end 402 | 403 | # Calculate the absolute path of the user's storage directory. 404 | # 405 | # user - The String username. 406 | # 407 | # Returns the String path: 408 | # e.g. '/data/repositories/2/a8/e2/95/mojombo'. 409 | def self.shard_user_path(user) 410 | hex = Digest::MD5.hexdigest(user) 411 | partition = partition_hex(user) 412 | shard = File.join(partition, hex[0..1], hex[2..3], hex[4..5]) 413 | File.join(REPO_DIR, shard, user) 414 | end 415 | 416 | def shard_user_path(user) 417 | Chimney.shard_user_path(user) 418 | end 419 | 420 | # Calculate the absolute path of the repo's storage directory. 421 | # 422 | # user - The String username. 423 | # repo - The String repo name. 424 | # 425 | # Returns the String path: 426 | # e.g. '/data/repositories/2/a8/e2/95/mojombo/god.git'. 427 | def self.shard_repo_path(user, repo) 428 | hex = Digest::MD5.hexdigest(user) 429 | partition = partition_hex(user) 430 | shard = File.join(partition, hex[0..1], hex[2..3], hex[4..5]) 431 | File.join(REPO_DIR, shard, user, "#{repo}.git") 432 | end 433 | 434 | def shard_repo_path(user, repo) 435 | Chimney.shard_repo_path(user, repo) 436 | end 437 | 438 | # Calculate the absolute path of the gist's storage directory. 439 | # 440 | # gist - The String gist ID. 441 | # 442 | # Returns String path: 443 | # e.g. '/data/repositories/0/81/dc/9b/gist/1234.git'. 444 | def self.shard_gist_path(gist) 445 | hex = Digest::MD5.hexdigest(gist) 446 | partition = partition_hex(gist) 447 | shard = File.join(partition, hex[0..1], hex[2..3], hex[4..5]) 448 | File.join(REPO_DIR, shard, 'gist', "#{gist}.git") 449 | end 450 | 451 | def shard_gist_path(gist) 452 | Chimney.shard_gist_path(gist) 453 | end 454 | 455 | # Calculate the partition hex digit. 456 | # 457 | # name - The String username or gist. 458 | # 459 | # Returns a single lowercase hex digit [0-9a-f] as a String. 460 | def self.partition_hex(name) 461 | Digest::MD5.hexdigest(name)[0].chr 462 | end 463 | 464 | def partition_hex(name) 465 | Chimney.partition_hex(name) 466 | end 467 | 468 | # The unix user account that has access to the repository data. 469 | # 470 | # Returns the String user e.g. 'root'. 471 | def self.unix_user 472 | UNIX_USER 473 | end 474 | 475 | def unix_user 476 | Chimney.unix_user 477 | end 478 | 479 | # The short name of the server currently executing this code. If this is a 480 | # front end and we're on fe2.rs.github.com, this will return "fe2". 481 | # 482 | # Returns a String host short name e.g. "fe2". 483 | def self.current_server 484 | if hostname =~ /github\.com/ 485 | hostname.split('.').first 486 | else 487 | "localhost" 488 | end 489 | end 490 | 491 | def current_server 492 | Chimney.current_server 493 | end 494 | 495 | # The full hostname of the current server. 496 | # 497 | # Returns a String hostname e.g. "fe2.rs.github.com". 498 | def self.hostname 499 | `hostname`.chomp 500 | end 501 | 502 | private 503 | 504 | # Ensure that a valid connection to the routing server has been made 505 | # and that the list of hosts has been fetched. 506 | # 507 | # Returns nothing. 508 | def ensure_client_connection 509 | logger.info "Starting Chimney..." 510 | self.client = Redis.new(:host => self.host, :port => self.port) 511 | if hosts = self.client.get('gh.storage.servers') 512 | self.hosts = hosts.split(',') 513 | write_hosts_to_file 514 | logger.info "Found #{self.hosts.size} hosts from Router." 515 | else 516 | read_hosts_from_file 517 | raise InvalidRoutingServer.new("Hosts could not be loaded.") if self.hosts.empty? 518 | logger.warn "Router does not contain hosts list; loaded #{self.hosts.size} hosts from file." 519 | end 520 | rescue Errno::ECONNREFUSED 521 | read_hosts_from_file 522 | raise InvalidRoutingServer.new("Hosts could not be loaded.") if self.hosts.empty? 523 | logger.warn "Unable to connect to Router; loaded #{self.hosts.size} hosts from file." 524 | end 525 | 526 | # Write the hosts list to a file. 527 | # 528 | # Returns nothing. 529 | def write_hosts_to_file 530 | File.open(SMOKE_HOSTS_FILE, 'w') do |f| 531 | f.write(self.hosts.join(',')) 532 | end 533 | end 534 | 535 | # Read the hosts from a file. 536 | # 537 | # Returns nothing. 538 | def read_hosts_from_file 539 | if File.exists?(SMOKE_HOSTS_FILE) 540 | self.hosts = File.read(SMOKE_HOSTS_FILE).split(',') 541 | end 542 | end 543 | 544 | # Reload the hosts list from the router. 545 | # 546 | # Returns nothing. 547 | def reload_hosts_list 548 | self.hosts = self.storage_servers 549 | write_hosts_to_file 550 | end 551 | 552 | # Find the storage server with the least disk usage for the target partition. 553 | # 554 | # type - Either :user or :gist. 555 | # name - The String username or gist. 556 | # 557 | # Returns a hostname. 558 | def find_least_loaded_host(name) 559 | partition = partition_hex(name) 560 | self.hosts.select { |h| storage_server_online?(h) }.map do |host| 561 | [self.client.get("gh.storage.server.usage.percent.#{host}.#{partition}").to_f, host] 562 | end.sort.first.last 563 | end 564 | 565 | # Set the route for a given user or gist. 566 | # 567 | # type - Either :user or :gist. 568 | # name - The String username or gist. 569 | # host - The String hostname that will be set if it is present (optional). 570 | # 571 | # Returns the String hostname that was set. 572 | def set_route(type, name, host = nil) 573 | if !host && existing_host = self.client.get("gh.storage.#{type}.#{name}") 574 | return existing_host 575 | end 576 | 577 | unless host 578 | reload_hosts_list 579 | host = find_least_loaded_host(name) 580 | end 581 | 582 | self.client.set("gh.storage.#{type}.#{name}", host) 583 | host 584 | end 585 | 586 | # Try to find a route using a variety of different fallbacks. 587 | # 588 | # type - Either :user or :gist. 589 | # name - The String username or gist. 590 | # 591 | # Returns the hostname of the storage server. 592 | def try_route(type, name) 593 | try_route_with_redis(type, name) 594 | end 595 | 596 | # Try the lookup from redis. If redis is unavailable, try 597 | # to do the lookup from internal cache. 598 | # 599 | # type - Either :user or :gist. 600 | # name - The String username or gist. 601 | # 602 | # Returns the hostname of the storage server. 603 | def try_route_with_redis(type, name) 604 | if host = self.client.get("gh.storage.#{type}.#{name}") 605 | logger.debug "Found host '#{host}' for #{type} '#{name}' from Router." 606 | self.cache[name] = host 607 | else 608 | self.cache.delete(name) 609 | end 610 | host 611 | rescue Errno::ECONNREFUSED 612 | logger.warn "No connection to Router..." 613 | try_route_with_internal_cache(type, name) 614 | end 615 | 616 | # Try the lookup from the internal route cache. If the key is not 617 | # in internal cache, try to reconnect to redis and redo the lookup. 618 | # 619 | # type - Either :user or :gist. 620 | # name - The String username or gist. 621 | # 622 | # Returns the hostname of the storage server. 623 | def try_route_with_internal_cache(type, name) 624 | if host = self.cache[name] 625 | logger.debug "Found '#{host}' for #{type} '#{name}' from Internal Cache." 626 | host 627 | else 628 | logger.warn "No entry in Internal Cache..." 629 | try_route_with_new_redis_connection(type, name) 630 | end 631 | end 632 | 633 | # Try the lookup with a new redis connection. If redis is still 634 | # unavailable, try each storage server in turn to look for the user/gist. 635 | # 636 | # type - Either :user or :gist. 637 | # name - The String username or gist. 638 | # 639 | # Returns the hostname of the storage server. 640 | def try_route_with_new_redis_connection(type, name) 641 | self.client.connect_to_server 642 | host = self.client.get("gh.storage.#{type}.#{name}") 643 | logger.debug "Found host '#{host}' for #{type} '#{name}' from Router after reconnect." 644 | host 645 | rescue Errno::ECONNREFUSED 646 | logger.warn "Still no connection to Router..." 647 | try_route_with_individual_storage_checks(type, name) 648 | end 649 | 650 | # Try the lookup by asking each storage server if the user or gist dir exists. 651 | # 652 | # type - Either :user or :gist. 653 | # name - The String username or gist. 654 | # 655 | # Returns the hostname of the storage server or nil. 656 | def try_route_with_individual_storage_checks(type, name) 657 | self.hosts.each do |host| 658 | logger.debug "Trying host '#{host}' via Smoke for existence of #{type} '#{name}'..." 659 | 660 | svc = smoke(host) 661 | exist = 662 | case type 663 | when :user 664 | svc.user_dir_exist?(name) 665 | when :gist 666 | svc.gist_dir_exist?(name) 667 | else false 668 | end 669 | 670 | if exist 671 | self.cache[name] = host 672 | logger.debug "Found host '#{host}' for #{type} '#{name}' from Smoke." 673 | return host 674 | end 675 | end 676 | logger.warn "No host found for #{type} '#{name}'." 677 | nil 678 | rescue Object => e 679 | logger.error "No host found for #{type} '#{name}' because of '#{e.message}'." 680 | nil 681 | end 682 | 683 | def smoke(host) 684 | BERTRPC::Service.new(host, 8149, 2).call.store 685 | end 686 | end 687 | 688 | class Math 689 | # Duplicate some text an abitrary number of times. 690 | # 691 | # text - The String to be duplicated. 692 | # count - The Integer number of times to duplicate the text. 693 | # 694 | # Examples 695 | # multiplex('Tom', 4) 696 | # # => 'TomTomTomTom' 697 | # 698 | # Returns the duplicated String. 699 | def multiplex(text, count) 700 | text * count 701 | end 702 | end 703 | end 704 | 705 | module GitHub 706 | class Jobs 707 | # Performs a job. 708 | # 709 | # Returns nothing. 710 | def perform 711 | end 712 | end 713 | end 714 | -------------------------------------------------------------------------------- /test/fixtures/multiplex.rb: -------------------------------------------------------------------------------- 1 | module TomDoc 2 | module Fixtures 3 | class Multiplex 4 | # Duplicate some text an abitrary number of times. 5 | # 6 | # text - The String to be duplicated. 7 | # count - The Integer number of times to duplicate the text. 8 | # 9 | # Examples 10 | # multiplex('Tom', 4) 11 | # # => 'TomTomTomTom' 12 | # 13 | # multiplex('Bo', 2) 14 | # # => 'BoBo' 15 | # 16 | # multiplex('Chris', -1) 17 | # # => nil 18 | # 19 | # Returns the duplicated String when the count is > 1. 20 | # Returns nil when the count is < 1. 21 | # Returns the atomic mass of the element as a Float. The value is in 22 | # unified atomic mass units. 23 | def multiplex(text, count) 24 | text * count 25 | end 26 | 27 | # Duplicate some text an abitrary number of times. 28 | # 29 | # Returns the duplicated String. 30 | def multiplex2(text, count) 31 | text * count 32 | end 33 | 34 | # Duplicate some text an abitrary number of times. 35 | # 36 | # Examples 37 | # multiplex('Tom', 4) 38 | # # => 'TomTomTomTom' 39 | # 40 | # multiplex('Bo', 2) 41 | # # => 'BoBo' 42 | def multiplex3(text, count) 43 | text * count 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/fixtures/simple.rb: -------------------------------------------------------------------------------- 1 | class Simple 2 | # Just a simple method. 3 | # 4 | # text - The String to return. 5 | # 6 | # Returns a String. 7 | def string(text) 8 | text 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/helper' 2 | 3 | class MethodsReporter < TomDoc::Reporters::Base 4 | def write_method(method, prefix = '') 5 | @buffer = [] if @buffer.is_a?(String) 6 | @buffer << method.name 7 | end 8 | end 9 | 10 | class GeneratorTest < TomDoc::Test 11 | 12 | def setup 13 | @generator = TomDoc::Generator.new(:report => MethodsReporter) 14 | end 15 | 16 | test "can ignore validation methods" do 17 | @generator.options[:validate] = false 18 | methods = @generator.generate(fixture(:chimney)) 19 | assert_equal 47, methods.size 20 | end 21 | 22 | test "ignores invalid methods" do 23 | @generator.options[:validate] = true 24 | methods = @generator.generate(fixture(:chimney)) 25 | assert_equal 39, methods.size 26 | end 27 | 28 | test "detects built-in constants" do 29 | assert @generator.constant?('Object') 30 | assert @generator.constant?('Kernel') 31 | assert @generator.constant?('String') 32 | end 33 | 34 | test "detects common constants" do 35 | assert @generator.constant?('Boolean') 36 | assert @generator.constant?('Test::Unit::TestCase') 37 | end 38 | 39 | test "picks up constants from the thing we're TomDocin'" do 40 | scope = { :Chimney => TomDoc::Scope.new('Chimney') } 41 | @generator.instance_variable_set(:@scopes, scope) 42 | assert @generator.constant?('Chimney') 43 | end 44 | 45 | test "ignores non-constants" do 46 | assert !@generator.constant?('Dog') 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | require 'test/fixtures/multiplex' 4 | 5 | require 'tomdoc' 6 | 7 | module TomDoc 8 | class Test < ::Test::Unit::TestCase 9 | def self.test(name, &block) 10 | define_method("test_#{name.gsub(/\W/,'_')}", &block) if block 11 | end 12 | 13 | def default_test 14 | end 15 | 16 | def fixture(name) 17 | @fixtures ||= {} 18 | @fixtures[name] ||= File.read("test/fixtures/#{name}.rb") 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/html_reporter_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/helper' 2 | 3 | class HTMLReporterTest < TomDoc::Test 4 | def setup 5 | @html = TomDoc::Generator.new(:report => TomDoc::Reporters::HTML).generate(fixture(:simple)) 6 | end 7 | 8 | test "works" do 9 | assert_equal < 11 |
  • Simple#string(text)
    Just a simple method.
    12 | 
    13 | text - The String to return.
    14 | 
    15 | Returns a String.
  • 16 | 17 | html 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/source_parser_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/helper' 2 | 3 | class ChimneySourceParserTest < TomDoc::Test 4 | def setup 5 | @parser = TomDoc::SourceParser.new 6 | @result = @parser.parse(fixture(:chimney)) 7 | 8 | @chimney = @result[:GitHub][:Chimney] 9 | end 10 | 11 | test "finds instance methods" do 12 | assert_equal 35, @chimney.instance_methods.size 13 | end 14 | 15 | test "attaches TomDoc" do 16 | m = @chimney.instance_methods.detect { |m| m.name == :get_user_route } 17 | assert_equal [:user], m.tomdoc.args.map { |a| a.name } 18 | end 19 | 20 | test "finds class methods" do 21 | assert_equal 9, @chimney.class_methods.size 22 | end 23 | 24 | test "finds namespaces" do 25 | assert @result[:GitHub][:Math] 26 | assert_equal 2, @result.keys.size 27 | assert_equal 3, @result[:GitHub].keys.size 28 | end 29 | 30 | test "finds methods in a namespace" do 31 | assert_equal 1, @result[:GitHub].class_methods.size 32 | end 33 | 34 | test "finds multiple classes in one file" do 35 | assert_equal 1, @result[:GitHub][:Math].instance_methods.size 36 | assert_equal 1, @result[:GitHub][:Jobs].instance_methods.size 37 | end 38 | end 39 | 40 | class SourceParserTest < TomDoc::Test 41 | def setup 42 | @parser = TomDoc::SourceParser.new 43 | end 44 | 45 | test "finds single class in one file" do 46 | result = @parser.parse(fixture(:simple)) 47 | 48 | assert result[:Simple] 49 | 50 | methods = result[:Simple].instance_methods 51 | assert_equal 1, methods.size 52 | assert_equal [:string], methods.map { |m| m.name } 53 | end 54 | 55 | test "finds single module in one file" 56 | test "finds module in a module" 57 | test "finds module in a class" 58 | test "finds class in a class" 59 | 60 | test "finds class in a module in a module" do 61 | result = @parser.parse(fixture(:multiplex)) 62 | klass = result[:TomDoc][:Fixtures][:Multiplex] 63 | assert klass 64 | assert_equal 3, klass.instance_methods.size 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/tomdoc_parser_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/helper' 2 | 3 | class TomDocParserTest < TomDoc::Test 4 | def setup 5 | @comment = TomDoc::TomDoc.new(< 'TomTomTomTom' 17 | # 18 | # multiplex('Bo', 2) 19 | # # => 'BoBo' 20 | # 21 | # multiplex('Chris', -1) 22 | # # => nil 23 | # 24 | # Returns the duplicated String when the count is > 1. 25 | # Returns the atomic mass of the element as a Float. The value is in 26 | # unified atomic mass units. 27 | # Returns nil when the count is < 1. 28 | # Raises ExpectedString if the first argument is not a String. 29 | # Raises ExpectedInteger if the second argument is not an Integer. 30 | comment 31 | 32 | @comment2 = TomDoc::TomDoc.new(< 'TomTomTomTom' 45 | # 46 | # multiplex('Bo', 2) 47 | # # => 'BoBo' 48 | comment3 49 | 50 | @comment4 = TomDoc::TomDoc.new(<[_and_...](args) 58 | # 59 | # field - A field name. 60 | comment4 61 | 62 | @comment5 = TomDoc::TomDoc.new(<[_and_...](args) 70 | 71 | field - A field name. 72 | comment5 73 | 74 | @comment6 = TomDoc::TomDoc.new('') 75 | 76 | end 77 | 78 | test "knows when TomDoc is invalid" do 79 | assert_raises TomDoc::InvalidTomDoc do 80 | @comment3.validate 81 | end 82 | end 83 | 84 | test "parses a description" do 85 | assert_equal "Duplicate some text an abitrary number of times.", 86 | @comment.description 87 | end 88 | 89 | test "parses args" do 90 | assert_equal 3, @comment.args.size 91 | end 92 | 93 | test "knows an arg's name" do 94 | assert_equal :text, @comment.args.first.name 95 | assert_equal :count, @comment.args[1].name 96 | assert_equal :reverse, @comment.args[2].name 97 | end 98 | 99 | test "knows an arg's description" do 100 | assert_equal 'The Integer number of times to duplicate the text.', 101 | @comment.args[1].description 102 | 103 | reverse = 'An optional Boolean indicating whether to reverse the' 104 | reverse << ' result text or not.' 105 | assert_equal reverse, @comment.args[2].description 106 | end 107 | 108 | test "knows an arg's optionality" do 109 | assert_equal false, @comment.args.first.optional? 110 | assert_equal true, @comment.args.last.optional? 111 | end 112 | 113 | test "knows what to do when there are no args" do 114 | assert_equal 0, @comment2.args.size 115 | end 116 | 117 | test "knows how many examples there are" do 118 | assert_equal 3, @comment.examples.size 119 | end 120 | 121 | test "knows each example" do 122 | assert_equal "multiplex('Bo', 2)\n# => 'BoBo'", 123 | @comment.examples[1].to_s 124 | end 125 | 126 | test "knows what to do when there are no examples" do 127 | assert_equal 0, @comment2.examples.size 128 | end 129 | 130 | test "knows how many return examples there are" do 131 | assert_equal 3, @comment.returns.size 132 | end 133 | 134 | test "knows if the method raises anything" do 135 | assert_equal 2, @comment.raises.size 136 | end 137 | 138 | test "knows each return example" do 139 | assert_equal "Returns the duplicated String when the count is > 1.", 140 | @comment.returns.first.to_s 141 | 142 | string = '' 143 | string << "Returns the atomic mass of the element as a Float. " 144 | string << "The value is in unified atomic mass units." 145 | assert_equal string, @comment.returns[1].to_s 146 | 147 | assert_equal "Returns nil when the count is < 1.", 148 | @comment.returns[2].to_s 149 | end 150 | 151 | test "knows what to do when there are no return examples" do 152 | assert_equal 0, @comment2.examples.size 153 | end 154 | 155 | test "knows what the method yields" do 156 | assert_equal "Yields the Integer index of the iteration.", @comment4.yields 157 | end 158 | 159 | test "knows if the method has alternate signatures" do 160 | assert_equal 1, @comment4.signatures.size 161 | assert_equal "find_by_[_and_...](args)", @comment4.signatures.first 162 | end 163 | 164 | test "knows the fields associated with signatures" do 165 | assert_equal 1, @comment4.signature_fields.size 166 | 167 | arg = @comment4.signature_fields.first 168 | assert_equal :field, arg.name 169 | assert_equal "A field name.", arg.description 170 | end 171 | 172 | test "can hande comments without comment marker" do 173 | assert_equal "Duplicate some text an abitrary number of times.", 174 | @comment5.description 175 | end 176 | 177 | test "does not throw errors with an empty comment" do 178 | assert_equal '', @comment6.raw 179 | assert_equal '', @comment6.tomdoc 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /tomdoc.gemspec: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/lib/tomdoc/version' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "tomdoc" 5 | s.version = TomDoc::VERSION 6 | s.date = Time.now.strftime('%Y-%m-%d') 7 | s.summary = "A TomDoc library for Ruby." 8 | s.homepage = "http://github.com/defunkt/tomdoc" 9 | s.email = "chris@ozmm.org" 10 | s.authors = [ "Tom Preston-Werner", "Chris Wanstrath" ] 11 | s.has_rdoc = false 12 | 13 | s.files = %w( README.md Rakefile LICENSE ) 14 | s.files += Dir.glob("lib/**/*") 15 | s.files += Dir.glob("bin/**/*") 16 | s.files += Dir.glob("man/**/*") 17 | s.files += Dir.glob("test/**/*") 18 | s.executables = %w( tomdoc ) 19 | 20 | s.add_dependency "ruby_parser", ">= 2.0.4" 21 | s.add_dependency "colored" 22 | 23 | s.description = < 3 34 | # 35 | module Math 36 | ... 37 | end 38 | 39 | 40 | Method Documentation 41 | -------------------- 42 | 43 | A quick example will serve to best illustrate the TomDoc method documentation 44 | format: 45 | 46 | # Duplicate some text an abitrary number of times. 47 | # 48 | # text - The String to be duplicated. 49 | # count - The Integer number of times to duplicate the text. 50 | # 51 | # Examples 52 | # 53 | # multiplex('Tom', 4) 54 | # # => 'TomTomTomTom' 55 | # 56 | # Returns the duplicated String. 57 | def multiplex(text, count) 58 | text * count 59 | end 60 | 61 | TomDoc for a specific method consists of a block of single comment markers (#) 62 | that appears directly above the method. There MUST NOT be a blank line between 63 | the comment block and the method definition. A TomDoc method block consists of 64 | a description section (required), an arguments section (required if the method 65 | takes any arguments), an examples section (optional), and a returns section 66 | (required). Lines that contain text MUST be separated from the comment 67 | marker by a single space. Lines that do not contain text SHOULD consist of 68 | just a comment marker (no trailing spaces). 69 | 70 | ### The Description Section 71 | 72 | The description section SHOULD be in plain sentences. Each sentence SHOULD end 73 | with a period. Good descriptions explain what the code does at a high level. 74 | Make sure to explain any unexpected behavior that the method may have, or any 75 | pitfalls that the user may experience. Lines SHOULD be wrapped at 80 76 | characters. 77 | 78 | If a method's description begins with "Public:" then that method will be 79 | considered part of the project's public API. For example: 80 | 81 | # Public: Initialize a new Widget. 82 | 83 | This annotation is designed to let developers know which methods are 84 | considered stable. You SHOULD use this to document the public API of your 85 | project. This information can then be used along with [Semantic 86 | Versioning](http://semver.org) to inform decisions on when major, minor, and 87 | patch versions should be incremented. 88 | 89 | If a method's description begins with "Deprecated:" then that method will be 90 | considered as deprecated and users will know that it will be removed in a 91 | future version. 92 | 93 | ### The Arguments Section 94 | 95 | The arguments section consists of a list of arguments. Each list item MUST be 96 | comprised of the name of the argument, a dash, and an explanation of the 97 | argument in plain sentences. The expected type (or types) of each argument 98 | SHOULD be clearly indicated in the explanation. When you specify a type, use 99 | the proper classname of the type (for instance, use 'String' instead of 100 | 'string' to refer to a String type). The dashes following each argument name 101 | should be lined up in a single column. Lines SHOULD be wrapped at 80 columns. 102 | If an explanation is longer than that, additional lines MUST be indented at 103 | least two spaces but SHOULD be indented to match the indentation of the 104 | explanation. For example: 105 | 106 | # element - The Symbol representation of the element. The Symbol should 107 | # contain only lowercase ASCII alpha characters. 108 | 109 | All arguments are assumed to be required. If an argument is optional, you MUST 110 | specify the default value: 111 | 112 | # host - The String hostname to bind (default: '0.0.0.0'). 113 | 114 | For hash arguments, you SHOULD enumerate each valid option in a way similar 115 | to how normal arguments are defined: 116 | 117 | # options - The Hash options used to refine the selection (default: {}): 118 | # :color - The String color to restrict by (optional). 119 | # :weight - The Float weight to restrict by. The weight should 120 | # be specified in grams (optional). 121 | 122 | ### The Examples Section 123 | 124 | The examples section MUST start with the word "Examples" on a line by 125 | itself. The next line SHOULD be blank. The following lines SHOULD be indented 126 | by two spaces (three spaces from the initial comment marker) and contain code 127 | that shows off how to call the method and (optional) examples of what it 128 | returns. Everything under the "Examples" line should be considered code, so 129 | make sure you comment out lines that show return values. Separate examples 130 | should be separated by a blank line. For example: 131 | 132 | # Examples 133 | # 134 | # multiplex('x', 4) 135 | # # => 'xxxx' 136 | # 137 | # multiplex('apple', 2) 138 | # # => 'appleapple' 139 | 140 | ### The Returns Section 141 | 142 | The returns section should explain in plain sentences what is returned from 143 | the method. The line MUST begin with "Returns". If only a single thing is 144 | returned, state the nature and type of the value. For example: 145 | 146 | # Returns the duplicated String. 147 | 148 | If several different types may be returned, list all of them. For example: 149 | 150 | # Returns the given element Symbol or nil if none was found. 151 | 152 | If the return value of the method is not intended to be used, then you should 153 | simply state: 154 | 155 | # Returns nothing. 156 | 157 | If the method raises exceptions that the caller may be interested in, add 158 | additional lines that explain each exception and under what conditions it may 159 | be encountered. The lines MUST begin with "Raises". For example: 160 | 161 | # Returns nothing. 162 | # Raises Errno::ENOENT if the file cannot be found. 163 | # Raises Errno::EACCES if the file cannot be accessed. 164 | 165 | Lines SHOULD be wrapped at 80 columns. Wrapped lines MUST be indented under 166 | the above line by at least two spaces. For example: 167 | 168 | # Returns the atomic mass of the element as a Float. The value is in 169 | # unified atomic mass units. 170 | 171 | 172 | Special Considerations 173 | ---------------------- 174 | 175 | ### Attributes 176 | 177 | Ruby's built in `attr_reader`, `attr_writer`, and `attr_accessor` require a 178 | bit more consideration. With TomDoc you SHOULD NOT use `attr_access` since it 179 | represents two methods with different signatures. Restricting yourself in this 180 | way also makes you think more carefully about the read vs. write behavior and 181 | whether each should be part of the Public API. 182 | 183 | Here is an example TomDoc for `attr_reader`. 184 | 185 | # Public: Get the user's name. 186 | # 187 | # Returns the String name of the user. 188 | attr_reader :name 189 | 190 | Here is an example TomDoc for `attr_writer`. The parameter name should be the 191 | same as the attribute name. 192 | 193 | # Set the user's name. 194 | # 195 | # name - The String name of the user. 196 | # 197 | # Returns nothing. 198 | attr_writer :name 199 | 200 | While this approach certainly takes up more space than listing dozens of 201 | attributes on a single line, it allows for individual documentation of each 202 | attribute. Attributes are an extremely important part of a class and should be 203 | treated with the same care as any other methods. -------------------------------------------------------------------------------- /try/example.rb: -------------------------------------------------------------------------------- 1 | # Public: An awesome class created just for you! xxx 2 | # 3 | # 4 | class MyClass 5 | 6 | # Public: When called, will hit the owner of the instance 7 | # 8 | # banana - A tasty fruit, high in potassium 9 | # 10 | # Examples: 11 | # hit_me(@banana) # => "Ouch! That really hurt!" 12 | # 13 | # Returns a bitch of a right hook 14 | def hit_me(banana) 15 | # ... haduken! 16 | end 17 | 18 | end 19 | 20 | --------------------------------------------------------------------------------