httpdoc 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Alexander Staubo
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.
data/README.markdown ADDED
@@ -0,0 +1,137 @@
1
+ Httpdoc is a very simple documentation generator for generating public API documentation for an HTTP-based web service, parsed from comments embedded in Ruby source code. It's API philosophy-agnostic and supports REST and such if you like. For example, an HTTP API can be described like so:
2
+
3
+ ## List stuff.
4
+ #
5
+ # @return A list of stuff in XML.
6
+ # @status 200
7
+ # @status 403 If you do not have permission to list the stuff.
8
+ # @example
9
+ # @request
10
+ # GET #{base_url}/create/31
11
+ #
12
+ # @response
13
+ # HTTP/1.1 200 OK
14
+ # Content-Type: application/xml; charset=utf-8
15
+ #
16
+ # <stuff>...</stuff>
17
+ # @end
18
+ #
19
+ def list
20
+ ...
21
+ end
22
+
23
+ Limitations
24
+ -----------
25
+
26
+ Httpdoc is currently limited to Rails applications. I plan to generalize the internals to support Sinatra and other frameworks, which should be quite trivial.
27
+
28
+ Since Httpdoc does not read Rails routes, it currently requires per-action URL paths to be explicitly written out in the documentation strings. I will be working on route inference.
29
+
30
+ For historic reasons, only Textile and HTML is supported in documentation fragments. I plan to phase out Textile and make Markdown the default format.
31
+
32
+ Format
33
+ ------
34
+
35
+ The documentation format is vaguely inspired by JavaDoc conventions.
36
+
37
+ All documentation comments are indicated by double hashes, like so:
38
+
39
+ ## This line introduces a documentation comment, and
40
+ # this line continues it.
41
+
42
+ This first part of the documentation comment is the main description of the class or action.
43
+
44
+ Attributes consist of a key and a value:
45
+
46
+ # @foo Bar
47
+
48
+ Here, `foo` is an attribute, and `Bar` is its value. Attributes can have multi-line values; a value ends when the next attribute starts or the entire documentation comment terminates.
49
+
50
+ Some attributes take two arguments, such as in the case of `param`:
51
+
52
+ # @param key Some key.
53
+
54
+ Here, `key` is a parameter name, `Some key` is its description. See below.
55
+
56
+ A controller class is preceded by a block which introduces the controller:
57
+
58
+ ## This API does stuff.
59
+ class StuffController < ApplicationController
60
+
61
+ A controller class supports two attributes, `title` and `url`:
62
+
63
+ * `@title [title]`: specify a heading for the API itself.
64
+
65
+ * `@url [url]`: a base URL or relative path for the API. This is combined with the `--base-url` option passed to Httpdoc on the command line. Currently Httpdoc does not infer this stuff from Rails routes, so it can be overridden here if the controller name does not match the actual path used.
66
+
67
+ For example:
68
+
69
+ ## This API does stuff.
70
+ #
71
+ # @title Stuff
72
+ # @url http://stuff.ly/api/
73
+ #
74
+ class StuffController < ApplicationController
75
+
76
+ Methods are described thusly:
77
+
78
+ ## Create stuff that can be shared with other users.
79
+ #
80
+ def create
81
+ ...
82
+ end
83
+
84
+ Methods support the following attributes:
85
+
86
+ * `@param [name] [description]`: which describes a parameter name.
87
+
88
+ * `@status [code] [description]`: describes a possible HTTP status code and its meaning.
89
+
90
+ * `@return [description]`: says what the action produces in terms of output.
91
+
92
+ * `@short [description]`: specifies a short description, usable as a title.
93
+
94
+ * `@url [url]`: specifies the URL of the HTTP call, either an absolute URL or a relative path. This is combined with the `@url` attribute on the class itself, and the `--base-url` option passed to Httpdoc on the command line. Currently Httpdoc does not infer this stuff from Rails routes, so it can be overridden here if the controller name does not match the actual path used. If not specified, the name of the method itself is assumed.
95
+
96
+ * `@example`: introduces an example block. It's terminated with an `@end` attribute. Within the example block, `@request` introduces the request, and `@response` the response.
97
+
98
+ Here's a complete example of a method doc:
99
+
100
+ ## Create stuff that can be shared with other users.
101
+ #
102
+ # @url create/:amount
103
+ # @short Create stuff
104
+ # @param amount The amount of stuff to create, an integer between 3 and 42.
105
+ # @return The URL to the stuff.
106
+ # @status 201
107
+ # @status 403 If you do not have permission to create the stuff.
108
+ # @example
109
+ # @request
110
+ # POST #{base_url}/create/31
111
+ #
112
+ # @response
113
+ # HTTP/1.1 201 Created
114
+ # Content-Type: text/plain; charset=utf-8
115
+ #
116
+ # http://#{base_url}/stuff/9953
117
+ # @end
118
+ #
119
+ def create
120
+ ...
121
+ end
122
+
123
+ Usage
124
+ -----
125
+
126
+ To generate documentation for a bunch of controllers:
127
+
128
+ httpdoc --base-url=http://mysite.com/api/v2/ --template=mytemplate --output-dir=doc/ app/controllers/api/v2/*.rb
129
+
130
+ Look in the `examples` directory for an example controller and its pre-generated documentation example. To generate the example controller's documentation, using the example template:
131
+
132
+ httpdoc -t examples/single_file_template.erb -o examples/ examples/*.rb
133
+
134
+ Requirements
135
+ ------------
136
+
137
+ * RedCloth
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/bin/httpdoc ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'httpdoc'
5
+
6
+ template_name = 'single_file'
7
+ base_url = 'http://example.org/'
8
+ output_directory = '.'
9
+
10
+ ARGV.options do |opts|
11
+ opts.banner = "Usage: #{File.basename($0)} [OPTIONS ...] [INPUT ...]"
12
+ opts.separator ""
13
+ opts.on("-t", "--template=TEMPLATE", String, "Specify template (default: #{template_name})") do |value|
14
+ template_name = value
15
+ end
16
+ opts.on("-o", "--output-dir=DIRECTORY", String, "Output directory (defaults to current directory)") do |value|
17
+ output_directory = value
18
+ end
19
+ opts.on("--base-url=URL", String, "Base URL of HTTP interface (eg., http://myapp.org/)") do |value|
20
+ base_url = value
21
+ end
22
+ opts.on("-h", "--help", "Show this help message.") do
23
+ puts opts
24
+ exit
25
+ end
26
+ opts.parse!
27
+ if ARGV.empty?
28
+ puts "Nothing to do. Run with -h for help."
29
+ exit
30
+ end
31
+ end
32
+
33
+ generator = Httpdoc::Generator.new
34
+ generator.output_directory = output_directory
35
+ generator.template_name = template_name
36
+ generator.input_paths = ARGV
37
+ generator.base_url = base_url
38
+ generator.generate!
@@ -0,0 +1,36 @@
1
+ module Httpdoc
2
+
3
+ class Generator
4
+
5
+ attr_accessor :base_url
6
+ attr_accessor :output_directory
7
+ attr_accessor :input_paths
8
+ attr_accessor :template_name
9
+
10
+ def generate!
11
+ file_names = @input_paths.map { |path|
12
+ path = path.gsub(/\/$/, '')
13
+ if File.file?(path)
14
+ path
15
+ else
16
+ Dir.glob("#{path}/**/*_controller.rb")
17
+ end
18
+ }.flatten
19
+ file_names.each do |file_name|
20
+ parser = RubyCommentParser.new(file_name)
21
+ parser.parse
22
+ if parser.controller
23
+ renderer = Rendering::SingleFileRenderer.new(:base_url => @base_url)
24
+ renderer.template_name = @template_name
25
+ output_filename = File.join(@output_directory, File.basename(file_name).gsub(/\.rb/, ''))
26
+ output_filename << ".html"
27
+ File.open(output_filename, "w") do |file|
28
+ file << renderer.render_controller(parser.controller)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,47 @@
1
+ module Httpdoc
2
+
3
+ class Parameter
4
+ attr_accessor :name
5
+ attr_accessor :description
6
+ end
7
+
8
+ class Example
9
+ attr_accessor :request
10
+ attr_accessor :response
11
+ end
12
+
13
+ class Status
14
+ attr_accessor :code
15
+ attr_accessor :description
16
+ end
17
+
18
+ class Action
19
+ def initialize
20
+ @parameters = []
21
+ @examples = []
22
+ @statuses = []
23
+ end
24
+
25
+ attr_accessor :url
26
+ attr_accessor :short_description
27
+ attr_accessor :description
28
+ attr_accessor :parameters
29
+ attr_accessor :examples
30
+ attr_accessor :statuses
31
+ attr_accessor :return
32
+ end
33
+
34
+ class Controller
35
+ def initialize
36
+ @actions = []
37
+ @constants = {}
38
+ end
39
+
40
+ attr_accessor :title
41
+ attr_accessor :description
42
+ attr_accessor :actions
43
+ attr_accessor :constants
44
+ attr_accessor :url
45
+ end
46
+
47
+ end
@@ -0,0 +1,166 @@
1
+ module Httpdoc
2
+
3
+ module ControllerDocParser
4
+
5
+ def self.parse(doc)
6
+ controller = Controller.new
7
+ if doc =~ /\A(.*?)^\s*@/m
8
+ controller.description = $1.strip
9
+ end
10
+ doc.scan(/@title\s+(.*?)(^[@]|\z)/m).each do |s|
11
+ controller.title = $1.strip
12
+ break
13
+ end
14
+ doc.scan(/@url\s+(.*?)(?=^@|\z)/m) do
15
+ controller.url = $1.strip
16
+ break
17
+ end
18
+ controller
19
+ end
20
+
21
+ end
22
+
23
+ module ActionDocParser
24
+
25
+ def self.parse(name, doc, actions = [])
26
+ action = Action.new
27
+ if doc =~ /\A(.*?)^\s*@/m
28
+ action.description = $1.strip
29
+ end
30
+ doc.scan(/@param\s+(.*?)(?:\s+(.*?))?(?=^\s*@|\z)/m) do
31
+ param = Parameter.new
32
+ param.name = $1
33
+ param.description = $2
34
+ action.parameters << param
35
+ end
36
+ doc.scan(/@status\s+(.*?)(?:\s+(.*?))?(?=^\s*@|\z)/m) do
37
+ status = Status.new
38
+ status.code = $1.strip
39
+ status.description = $2.strip
40
+ action.statuses << status
41
+ end
42
+ doc.scan(/@return\s+(.*?)(?=^@|\z)/m) do
43
+ action.return = $1.strip
44
+ break
45
+ end
46
+ doc.scan(/@short\s+(.*?)(?=^@|\z)/m) do
47
+ action.short_description = $1.strip
48
+ break
49
+ end
50
+ doc.scan(/@url\s+(.*?)(?=^@|\z)/m) do
51
+ action.url = $1.strip
52
+ break
53
+ end
54
+ action.url ||= name
55
+ doc.scan(/@example\s*\n(.*?)(^\s*@end|\z)/m) do
56
+ example = Example.new
57
+ s = $1
58
+ s.scan(/(^[\t ]*)@request\s+(.*?)(?=^\s*@|\z)/m) do
59
+ padding, req = $1, $2
60
+ example.request = req.split("\n").map { |line|
61
+ line = line[padding.length..-1] || '' if line[0, padding.length] == padding
62
+ line
63
+ }.join("\n")
64
+ break
65
+ end
66
+ s.scan(/(^[\t ]*)@response\s+(.*?)(?=^\s*@|\z)/m) do
67
+ padding, res = $1, $2
68
+ example.response = res.split("\n").map { |line|
69
+ line = (line[padding.length..-1] || '') if line[0, padding.length] == padding
70
+ line
71
+ }.join("\n")
72
+ break
73
+ end
74
+ action.examples << example
75
+ break
76
+ end
77
+ actions << action
78
+ actions
79
+ end
80
+
81
+ end
82
+
83
+ class RubyCommentParser
84
+
85
+ def initialize(file_name)
86
+ @file_name = file_name
87
+ end
88
+
89
+ def parse
90
+ reset
91
+ File.open(@file_name) do |file|
92
+ file.readlines.each do |line|
93
+ case @state
94
+ when :top
95
+ case line
96
+ when /\A\s*##(.*)/
97
+ @buffer << $1
98
+ @buffer << "\n"
99
+ @state = :class_doc
100
+ end
101
+ when :class_doc
102
+ case line
103
+ when /\A\s*#+\s?(.*)/
104
+ @buffer << $1
105
+ @buffer << "\n"
106
+ when /(^|\s*)class \w/
107
+ unless @buffer.empty?
108
+ @controller = ControllerDocParser.parse(@buffer)
109
+ @buffer = ''
110
+ end
111
+ @state = :class_def
112
+ end
113
+ when :class_def
114
+ case line
115
+ when /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)/
116
+ if @controller
117
+ @controller.constants[$1] = $2
118
+ end
119
+ when /\A\s*##(.*)/
120
+ @buffer << $1
121
+ @state = :method_def
122
+ end
123
+ when :method_def
124
+ case line
125
+ when /\A\s*#+\s?(.*)/
126
+ @buffer << $1
127
+ @buffer << "\n"
128
+ when /\A\s*def ([^\s#\(]+)/
129
+ name = $1
130
+ unless @buffer.empty?
131
+ if @controller
132
+ ActionDocParser.parse(name, @buffer, @controller.actions)
133
+ end
134
+ @buffer = ''
135
+ end
136
+ @state = :class_def
137
+ else
138
+ unless @buffer.empty?
139
+ if @controller
140
+ ActionDocParser.parse(nil, @buffer, @controller.actions)
141
+ end
142
+ @buffer = ''
143
+ end
144
+ @state = :class_def
145
+ end
146
+ end
147
+ end
148
+ end
149
+ if @controller
150
+ @controller.url ||= $1 if @file_name =~ /(\w+)_controller\./
151
+ end
152
+ end
153
+
154
+ attr_reader :controller
155
+
156
+ private
157
+
158
+ def reset
159
+ @controller = nil
160
+ @buffer = ''
161
+ @state = :top
162
+ end
163
+
164
+ end
165
+
166
+ end
@@ -0,0 +1,156 @@
1
+ require "uri"
2
+ require "erb"
3
+ require "redcloth"
4
+
5
+ module Httpdoc
6
+ module Rendering
7
+
8
+ class UndefinedVariableError < Exception
9
+ def initialize(name)
10
+ super("Undefined variable #{name}")
11
+ @name = name
12
+ end
13
+ attr_reader :name
14
+ end
15
+
16
+ class UndefinedConstantError < Exception
17
+ def initialize(name)
18
+ super("Undefined constant #{name}")
19
+ @name = name
20
+ end
21
+ attr_reader :name
22
+ end
23
+
24
+ class ControllerContext
25
+
26
+ def initialize(renderer, controller)
27
+ @renderer = renderer
28
+ @controller = controller
29
+ end
30
+
31
+ def h(s)
32
+ s ||= ''
33
+ return s.gsub(/#\{(.*)\}/) {
34
+ name = $1
35
+ case name
36
+ when /^[A-Z0-9_]+$/
37
+ value = @controller.constants[name]
38
+ raise UndefinedConstantError.new(name) unless value
39
+ value = eval(value)
40
+ when "base_url"
41
+ value = base_url.gsub(/\/$/, '')
42
+ else
43
+ raise UndefinedVariableError, name
44
+ end
45
+ value
46
+ }
47
+ end
48
+
49
+ def doc_fragment_to_html(s)
50
+ s = s.gsub("\n", ' ')
51
+ RedCloth.new(s).to_html
52
+ end
53
+
54
+ def expand_url_with_subtitutions(url)
55
+ url = [base_url, url].join("/") unless url =~ /^\//
56
+ return URI.join(@renderer.base_url, url).to_s.gsub(/:([\w_]+)/, '<strong>&lt;\1&gt;</strong>')
57
+ end
58
+
59
+ def base_url
60
+ controller_url = @controller.url
61
+ controller_url ||= "/"
62
+ return URI.join(@renderer.base_url, controller_url).to_s
63
+ end
64
+
65
+ def escape_html(h)
66
+ return nil unless h
67
+ h = h.dup
68
+ h.gsub!("&", "&amp;")
69
+ h.gsub!("<", "&lt;")
70
+ h.gsub!(">", "&gt;")
71
+ h.gsub!("\n", "<br/>")
72
+ h
73
+ end
74
+
75
+ def format_request(req)
76
+ envelope, body = req.split("\n\n")
77
+ lines = envelope.split("\n")
78
+ first_line, header_lines = lines[0], (lines[1..-1] || [])
79
+ result = "<div class='request'>"
80
+ result << "<strong class='request_first_line'>#{h(first_line)}</strong><br/>"
81
+ result << header_lines.map { |h|
82
+ name, value = h.split(":\s*")
83
+ "<span class='request_header_line'><strong class='request_header_name'>#{escape_html(name)}" <<
84
+ "</strong>: <span class='request_header_value'>#{escape_html(value)}</span></span>"
85
+ }.join("<br/>")
86
+ if body and body != ''
87
+ result << "<br/>"
88
+ result << "<div class='request_body'>#{escape_html(body.strip)}</div>"
89
+ end
90
+ result << "</div>"
91
+ result
92
+ end
93
+
94
+ def format_response(res)
95
+ envelope, body = res.split("\n\n")
96
+ lines = envelope.split("\n")
97
+ first_line, header_lines = lines[0], (lines[1..-1] || [])
98
+ result = "<div class='response'>"
99
+ result << "<strong class='response_first_line'>#{h(first_line)}</strong><br/>"
100
+ result << header_lines.map { |h|
101
+ name, value = h.scan(/(.+):\s*(.+)/)[0]
102
+ "<span class='response_header_line'><strong class='response_header_name'>#{escape_html(name)}" <<
103
+ "</strong>: <span class='response_header_value'>#{escape_html(value)}</span></span>"
104
+ }.join("<br/>")
105
+ if body and body != ''
106
+ result << "<br/><br/>"
107
+ result << "<div class='response_body'>#{escape_html(body)}</div>"
108
+ end
109
+ result << "</div>"
110
+ result
111
+ end
112
+
113
+ def get_binding
114
+ return binding
115
+ end
116
+
117
+ attr_reader :controller
118
+
119
+ end
120
+
121
+ class SingleFileRenderer
122
+
123
+ def initialize(options = {})
124
+ @base_url = options[:base_url]
125
+ @base_url ||= 'http://example.com/'
126
+ end
127
+
128
+ def render_controller(controller)
129
+ %w(erb).each do |extension|
130
+ template_file_name = "#{@template_name}"
131
+ template_file_name = "#{template_file_name}.#{extension}" unless template_file_name =~ /#{Regexp.escape(extension)}$/
132
+ unless File.exist?(template_file_name)
133
+ possible_template_file_name = File.join(File.dirname(__FILE__), "templates/#{template_file_name}")
134
+ if File.exist?(possible_template_file_name)
135
+ template_file_name = possible_template_file_name
136
+ end
137
+ end
138
+ template_content = File.read(template_file_name)
139
+ context = ControllerContext.new(self, controller)
140
+ erb = ERB.new(template_content, nil, "-")
141
+ return erb.result(context.get_binding)
142
+ end
143
+ end
144
+
145
+ def find_template(name)
146
+ %(erb).each do |extension|
147
+ end
148
+ end
149
+
150
+ attr_reader :base_url
151
+ attr_accessor :template_name
152
+
153
+ end
154
+
155
+ end
156
+ end
data/lib/httpdoc.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "httpdoc/generator"
2
+ require "httpdoc/model"
3
+ require "httpdoc/parser"
4
+ require "httpdoc/rendering"
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: httpdoc
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
+ platform: ruby
12
+ authors:
13
+ - Alexander Staubo
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-23 00:00:00 +01:00
19
+ default_executable: httpdoc
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: RedCloth
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: Simple documentation generator for publishing APIs from Rails applications.
36
+ email: alex@bengler.no
37
+ executables:
38
+ - httpdoc
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.markdown
44
+ files:
45
+ - LICENSE
46
+ - README.markdown
47
+ - VERSION
48
+ - lib/httpdoc.rb
49
+ - lib/httpdoc/generator.rb
50
+ - lib/httpdoc/model.rb
51
+ - lib/httpdoc/parser.rb
52
+ - lib/httpdoc/rendering.rb
53
+ - bin/httpdoc
54
+ has_rdoc: false
55
+ homepage: http://github.com/origo/httpdoc
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options: []
60
+
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ hash: 3
69
+ segments:
70
+ - 0
71
+ version: "0"
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.3.7
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: Simple documentation generator for publishing APIs from Rails applications.
88
+ test_files: []
89
+