]> gitweb.fluxo.info Git - leap/leap_cli.git/commitdiff
initial experiments with auto doc creation
authorelijah <elijah@riseup.net>
Mon, 10 Dec 2012 06:24:57 +0000 (22:24 -0800)
committerelijah <elijah@riseup.net>
Sun, 24 Feb 2013 00:46:28 +0000 (16:46 -0800)
Rakefile
lib/leap_cli/app.rb [new file with mode: 0644]
lib/lib_ext/markdown_document_listener.rb [new file with mode: 0644]

index 21d1c2d85c5c9dd3019dd580ba3d08b427f9993d..54ae637f7202b9da76594175bb1b8d00d1c59292 100644 (file)
--- a/Rakefile
+++ b/Rakefile
@@ -27,6 +27,8 @@ $spec_path = 'leap_cli.gemspec'
 $base_dir  = File.dirname(__FILE__)
 $spec      = eval(File.read(File.join($base_dir, $spec_path)))
 $gem_path  = File.join($base_dir, 'pkg', "#{$spec.name}-#{$spec.version}.gem")
+$lib_dir   = "#{$base_dir}/lib"
+$LOAD_PATH.unshift $lib_dir
 
 def built_gem_path
   Dir[File.join($base_dir, "#{$spec.name}-*.gem")].sort_by{|f| File.mtime(f)}.last
@@ -122,3 +124,109 @@ end
 #   rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
 #   rd.title = 'Your application title'
 # end
+
+desc "Generate documentation"
+task 'doc' do
+  require 'leap_cli'
+  require 'leap_cli/app'
+
+  class DocMaker < GLI::Command
+    def initialize(app)
+      @app = app
+      @listener = GLI::Commands::RdocDocumentListener.new([],[],[])
+    end
+
+    def create
+      @listener.beginning
+      @listener.program_desc(@app.program_desc) unless @app.program_desc.nil?
+      @listener.program_long_desc(@app.program_long_desc) unless @app.program_long_desc.nil?
+      @listener.version(@app.version_string)
+      if any_options?(@app)
+        @listener.options
+      end
+      document_flags_and_switches(@listener, @app.flags.values.sort(&by_name), @app.switches.values.sort(&by_name))
+      if any_options?(@app)
+        @listener.end_options
+      end
+      @listener.commands
+      document_commands(@listener, @app)
+      @listener.end_commands
+      @listener.ending
+    end
+
+    private
+
+    def document_commands(document_listener,context)
+      context.commands.values.reject {|_| _.nodoc }.sort(&by_name).each do |command|
+        call_command_method_being_backwards_compatible(document_listener,command)
+        document_listener.options if any_options?(command)
+        document_flags_and_switches(document_listener,command_flags(command),command_switches(command))
+        document_listener.end_options if any_options?(command)
+        document_listener.commands if any_commands?(command)
+        document_commands(document_listener,command)
+        document_listener.end_commands if any_commands?(command)
+        document_listener.end_command(command.name)
+      end
+      document_listener.default_command(context.get_default_command)
+    end
+
+    def call_command_method_being_backwards_compatible(document_listener,command)
+      command_args = [command.name,
+                      Array(command.aliases),
+                      command.description,
+                      command.long_description,
+                      command.arguments_description]
+      if document_listener.method(:command).arity == 6
+        command_args << command.arguments_options
+      end
+      document_listener.command(*command_args)
+    end
+
+    def by_name
+      lambda { |a,b| a.name.to_s <=> b.name.to_s }
+    end
+
+    def command_flags(command)
+      command.topmost_ancestor.flags.values.select { |flag| flag.associated_command == command }.sort(&by_name)
+    end
+
+    def command_switches(command)
+      command.topmost_ancestor.switches.values.select { |switch| switch.associated_command == command }.sort(&by_name)
+    end
+
+    def document_flags_and_switches(document_listener,flags,switches)
+      flags.each do |flag|
+        document_listener.flag(flag.name,
+                               Array(flag.aliases),
+                               flag.description,
+                               flag.long_description,
+                               flag.safe_default_value,
+                               flag.argument_name,
+                               flag.must_match,
+                               flag.type)
+      end
+      switches.each do |switch|
+        document_listener.switch(switch.name,
+                                 Array(switch.aliases),
+                                 switch.description,
+                                 switch.long_description,
+                                 switch.negatable)
+      end
+    end
+
+    def any_options?(context)
+      options = if context.kind_of?(GLI::Command)
+                  command_flags(context) + command_switches(context)
+                else
+                  context.flags.values + context.switches.values
+                end
+      !options.empty?
+    end
+
+    def any_commands?(command)
+      !command.commands.empty?
+    end
+  end
+
+  puts DocMaker.new(LeapCli::Commands).create
+end
diff --git a/lib/leap_cli/app.rb b/lib/leap_cli/app.rb
new file mode 100644 (file)
index 0000000..90c4ae9
--- /dev/null
@@ -0,0 +1,57 @@
+require 'gli'
+require 'highline'
+require 'forwardable'
+require 'lib_ext/gli' # our custom extensions to gli
+
+#
+# Typically, GLI and Highline methods are loaded into the global namespace.
+# Instead, here we load these into the module LeapCli::Commands in order to
+# ensure that the cli logic and code is kept isolated to leap_cli/commands/*.rb
+#
+# no cheating!
+#
+module LeapCli::Commands
+  extend GLI::App
+  extend Forwardable
+
+  #
+  # delegate highline methods to make them available to sub-commands
+  #
+  @terminal = HighLine.new
+  def_delegator :@terminal, :ask,    'self.ask'
+  def_delegator :@terminal, :agree,  'self.agree'
+  def_delegator :@terminal, :choose, 'self.choose'
+  def_delegator :@terminal, :say,    'self.say'
+  def_delegator :@terminal, :color,  'self.color'
+  def_delegator :@terminal, :list,   'self.list'
+
+  #
+  # make config manager available as 'manager'
+  #
+  def self.manager
+    @manager ||= begin
+      manager = LeapCli::Config::Manager.new
+      manager.load
+      manager
+    end
+  end
+
+  #
+  # info about leap command line suite
+  #
+  program_desc       LeapCli::SUMMARY
+  program_long_desc  LeapCli::DESCRIPTION
+
+  #
+  # handle --version ourselves
+  #
+  if ARGV.grep(/--version/).any?
+    puts "leap #{LeapCli::VERSION}, ruby #{RUBY_VERSION}"
+    exit(0)
+  end
+
+  #
+  # load commands and run
+  #
+  commands_from('leap_cli/commands')
+end
diff --git a/lib/lib_ext/markdown_document_listener.rb b/lib/lib_ext/markdown_document_listener.rb
new file mode 100644 (file)
index 0000000..55026e9
--- /dev/null
@@ -0,0 +1,122 @@
+require 'stringio'
+require 'gli/commands/help_modules/arg_name_formatter'
+
+#
+# adaption of RdocDocumentListener to use Markdown
+# see http://rtomayko.github.com/ronn/ronn-format.7
+#
+
+module GLI
+  module Commands
+    class MarkdownDocumentListener
+
+      def initialize(global_options,options,arguments)
+        @io = STDOUT #File.new(File.basename($0) + ".rdoc",'w')
+        @nest = ''
+        @arg_name_formatter = GLI::Commands::HelpModules::ArgNameFormatter.new
+      end
+
+      def beginning
+      end
+
+      # Called when processing has completed
+      def ending
+        #@io.close
+      end
+
+      # Gives you the program description
+      def program_desc(desc)
+        @io.puts "== #{File.basename($0)} - #{desc}"
+        @io.puts
+      end
+
+      def program_long_desc(desc)
+        @io.puts desc
+        @io.puts
+      end
+
+      # Gives you the program version
+      def version(version)
+        @io.puts "v#{version}"
+        @io.puts
+      end
+
+      def options
+        if @nest.size == 0
+          @io.puts "=== Global Options"
+        else
+          @io.puts "#{@nest}=== Options"
+        end
+      end
+
+      # Gives you a flag in the current context
+      def flag(name,aliases,desc,long_desc,default_value,arg_name,must_match,type)
+        invocations = ([name] + Array(aliases)).map { |_| add_dashes(_) }.join('|')
+        usage = "#{invocations} #{arg_name || 'arg'}"
+        @io.puts "#{@nest}=== #{usage}"
+        @io.puts
+        @io.puts String(desc).strip
+        @io.puts
+        @io.puts "[Default Value] #{default_value || 'None'}"
+        @io.puts "[Must Match] #{must_match.to_s}" unless must_match.nil?
+        @io.puts String(long_desc).strip
+        @io.puts
+      end
+
+      # Gives you a switch in the current context
+      def switch(name,aliases,desc,long_desc,negetable)
+        if negetable
+          name = "[no-]#{name}" if name.to_s.length > 1
+          aliases = aliases.map { |_|  _.to_s.length > 1 ? "[no-]#{_}" : _ }
+        end
+        invocations = ([name] + aliases).map { |_| add_dashes(_) }.join('|')
+        @io.puts "#{@nest}=== #{invocations}"
+        @io.puts String(desc).strip
+        @io.puts
+        @io.puts String(long_desc).strip
+        @io.puts
+      end
+
+      def end_options
+      end
+
+      def commands
+        @io.puts "#{@nest}=== Commands"
+        @nest = "#{@nest}="
+      end
+
+      # Gives you a command in the current context and creates a new context of this command
+      def command(name,aliases,desc,long_desc,arg_name,arg_options)
+        @io.puts "#{@nest}=== Command: <tt>#{([name] + aliases).join('|')} #{@arg_name_formatter.format(arg_name,arg_options)}</tt>"
+        @io.puts String(desc).strip
+        @io.puts
+        @io.puts String(long_desc).strip
+        @nest = "#{@nest}="
+      end
+
+      # Ends a command, and "pops" you back up one context
+      def end_command(name)
+        @nest.gsub!(/=$/,'')
+      end
+
+      # Gives you the name of the current command in the current context
+      def default_command(name)
+        @io.puts "[Default Command] #{name}" unless name.nil?
+      end
+
+      def end_commands
+        @nest.gsub!(/=$/,'')
+      end
+
+    private
+
+      def add_dashes(name)
+        name = "-#{name}"
+        name = "-#{name}" if name.length > 2
+        name
+      end
+
+
+    end
+  end
+end