Permalink
Browse files

Initial implementation, documents route and error definitions.

* Doesn't record the definition itself yet.
* Documents but doesn't properly nest modular definitions.
  • Loading branch information...
1 parent 82755c7 commit d26a8243336e08f91acf137bdd1475718e475032 @rue rue committed Nov 11, 2011
Showing with 575 additions and 0 deletions.
  1. +7 −0 lib/rdoc/discover.rb
  2. +148 −0 lib/rdoc/parser/sinatra.rb
  3. +420 −0 test/test_rdoc_parser_sinatra.rb
View
7 lib/rdoc/discover.rb
@@ -0,0 +1,7 @@
+begin
+ gem "rdoc", "~> 3"
+ require "rdoc/parser/sinatra"
+
+rescue Gem::LoadError
+ # Meh
+end
View
148 lib/rdoc/parser/sinatra.rb
@@ -0,0 +1,148 @@
+# -*- encoding: utf-8 -*-
+
+require "rubygems"
+
+gem "rdoc", "~> 3"
+ require "rdoc"
+ require "rdoc/parser/ruby"
+
+
+#
+# Artificial scope for top-level routes.
+#
+class RDoc::SinatraRoutes < RDoc::AnonClass#RDoc::NormalModule
+end
+
+#
+# Sinatra route definition as a method.
+#
+class RDoc::SinatraRoute < RDoc::AnyMethod
+
+ def initialize(route_definition, content_text)
+ super(content_text, route_definition)
+
+ @params = ""
+ end
+
+ def aref_prefix
+ ""
+ end
+end
+
+#
+# Sinatra routing error definition as a method.
+#
+class RDoc::SinatraRouteError < RDoc::SinatraRoute
+end
+
+
+#
+# An augmented Ruby parser for Sinatra projects.
+#
+# In addition to normal Ruby doc, documentation is also extracted
+# for route definitions.
+#
+class RDoc::Parser::Sinatra < RDoc::Parser::Ruby
+
+ # Re-declare to force overriding the normal .rb handler.
+ parse_files_matching /\.rbw?$/i
+
+
+ HTTP_VERBS = %w[GET HEAD POST PUT PATCH DELETE OPTIONS]
+ HTTP_ERRORS = {"NOT_FOUND" => 404, "ERROR" => 500}
+
+
+ #
+ # New parser, adds a top-level Application Routes.
+ #
+ def initialize(top_level, file_name, content, options, stats)
+ super
+
+ # Tuck away our little special module
+ @routes = @top_level.add_module RDoc::SinatraRoutes, "Application Routes"
+
+ @current_route = nil
+ end
+
+ #
+ # Override normal meta-method parsing to handle Sinatra routes and errors.
+ #
+ def parse_meta_method(container, single, token, comment)
+ name = token.name.upcase
+
+ case name
+ when *HTTP_VERBS
+ r = parse_route_definition token
+ r.comment = comment
+ when "NOT_FOUND", "ERROR"
+ r = parse_error_definition token
+ r.comment = comment
+ else
+ super
+ end
+ end
+
+
+ private
+
+ def parse_route_definition(http_method_token)
+ start_collecting_tokens
+ add_token http_method_token
+
+ token_listener(self) {
+ skip_tkspace false
+
+ pattern_token = get_tk
+ route_pattern = pattern_token.text
+
+ route_name = "#{http_method_token.name.upcase} #{route_pattern}"
+
+ if r = @routes.find_instance_method_named(route_name)
+ warn "Redefining route #{route_name}"
+ @current_route = r
+ else
+ @current_route = RDoc::SinatraRoute.new route_name, tokens_to_s
+ @routes.add_method @current_route
+ @stats.add_method @current_route
+ end
+ }
+
+ @current_route
+ end
+
+ def parse_error_definition(error_token)
+ start_collecting_tokens
+ add_token error_token
+
+ token_listener(self) {
+ skip_tkspace false
+
+ pattern_token = get_tk
+
+ status_codes = if TkDO === pattern_token
+ HTTP_ERRORS["ERROR"]
+ else
+ pattern_token.text
+ end
+
+ if error_token.name == "not_found"
+ route_name = "error #{HTTP_ERRORS["NOT_FOUND"]}"
+ else
+ route_name = "error #{status_codes}"
+ end
+
+ if r = @routes.find_instance_method_named(route_name)
+ warn "Redefining error #{error_token.name} #{pattern_token}"
+ @current_route = r
+ else
+ @current_route = RDoc::SinatraRouteError.new route_name, tokens_to_s
+ @routes.add_method @current_route
+ @stats.add_method @current_route
+ end
+ }
+
+ @current_route
+ end
+
+
+end
View
420 test/test_rdoc_parser_sinatra.rb
@@ -0,0 +1,420 @@
+# -*- encoding: utf-8 -*-
+
+require "stringio"
+require "tempfile"
+
+require "rubygems"
+
+gem "rdoc", "~> 3"
+ require "rdoc"
+ require "rdoc/parser/sinatra"
+
+gem "minitest"
+ require "minitest/autorun"
+
+
+class TestRDocParserSinatra < MiniTest::Unit::TestCase
+
+ def setup
+ @tempfile = Tempfile.new self.class.name
+ @filename = @tempfile.path
+
+ RDoc::TopLevel.reset
+ @top_level = RDoc::TopLevel.new @filename
+
+ @options = RDoc::Options.new
+ @options.quiet = true
+ @stats = RDoc::Stats.new 0
+
+ @route_definitions = @routes = nil
+ end
+
+ def teardown
+ @tempfile.close
+ end
+
+
+
+ def test_routes_stored_in_top_level_application_routes_class_hiding_in_modules
+ app = <<-APP
+ require "sinatra"
+
+ ##
+ # Route 1
+ #
+ get "/foo" do
+ :hi
+ end
+ APP
+
+ make_a_parser_for app
+ @parser.scan
+
+ r = RDoc::TopLevel.find_module_named "Application Routes"
+ r.wont_be_nil
+
+ r.method_list.first.name.must_equal 'GET "/foo"'
+ end
+
+ def test_route_presented_as_http_method_and_route_pattern
+ app = <<-APP
+ require "sinatra"
+
+ ##
+ # Route 2
+ #
+ # Some more text goes here.
+ #
+ get "/foo" do
+ :hi
+ end
+ APP
+
+ make_a_parser_for app
+ extract_routes
+
+ r = @routes.first
+
+ r.name.must_equal 'GET "/foo"'
+ r.comment.text.must_equal "Route 2\n\nSome more text goes here."
+ end
+
+ def test_route_pattern_may_be_regexp
+ app = <<-APP
+ require "sinatra"
+
+ ##
+ # Route 3
+ #
+ get /foo/ do
+ :hi
+ end
+
+ ##
+ # Route 4
+ #
+ get %r{bar} do
+ :ho
+ end
+ APP
+
+ make_a_parser_for app
+ extract_routes
+
+ @routes.size.must_equal 2
+
+ r = @routes.first
+ r.name.must_equal 'GET /foo/'
+ r.comment.text.must_equal "Route 3"
+
+ r = @routes.last
+ r.name.must_equal 'GET %r{bar}'
+ r.comment.text.must_equal "Route 4"
+ end
+
+
+ %w[GET HEAD POST PUT PATCH DELETE OPTIONS].each {|http|
+ define_method("test_parses_#{http}_definition") {
+ make_a_parser_for define_all_the_things!
+ extract_routes
+
+ @routes.find {|r| r.name.include? http }.wont_be_nil
+ }
+ }
+
+ def test_parses_error_definitions_without_status_code_defaulting_to_500
+ app = <<-APP
+ require "sinatra"
+
+ ##
+ # Gettersy
+ #
+ get "foo" do
+ :hi
+ end
+
+ ##
+ # OMG ERROR
+ #
+ error do
+ :haha
+ end
+ APP
+
+ make_a_parser_for app
+ extract_routes
+
+ @routes.size.must_equal 2
+
+ r = @routes.last
+ r.name.must_equal "error 500"
+ end
+
+ def test_parses_error_definitions_with_status_codes
+ app = <<-APP
+ require "sinatra"
+
+ ##
+ # Getters
+ #
+ get "foo" do
+ :hi
+ end
+
+ ##
+ # Fourohthree
+ #
+ error 403 do
+ :haha
+ end
+ APP
+
+ make_a_parser_for app
+ extract_routes
+
+ @routes.size.must_equal 2
+
+ r = @routes.last
+ r.name.must_equal "error 403"
+ end
+
+ def test_parses_not_found_definition_into_a_404_error
+ app = <<-APP
+ require "sinatra"
+
+ ##
+ # Route 5
+ #
+ get "foo" do
+ :hi
+ end
+
+ ##
+ # Route 6
+ #
+ not_found do
+ :haha
+ end
+ APP
+
+ make_a_parser_for app
+ extract_routes
+
+ @routes.size.must_equal 2
+
+ r = @routes.last
+ r.name.must_equal "error 404"
+ end
+
+ def test_parses_same_route_pattern_with_different_methods_as_separate
+ app = <<-APP
+ require "sinatra"
+
+ ##
+ # Route 7
+ #
+ get "foo" do
+ :hi
+ end
+
+ ##
+ # Route 8
+ #
+ put "foo" do
+ :ho
+ end
+ APP
+
+ make_a_parser_for app
+ extract_routes
+
+ @routes.size.must_equal 2
+
+ r = @routes.first
+ r.name.must_equal 'GET "foo"'
+
+ r = @routes.last
+ r.name.must_equal 'PUT "foo"'
+ end
+
+ def test_parses_same_route_pattern_with_same_method_as_the_same_and_latter_overrides
+ app = <<-APP
+ require "sinatra"
+
+ ##
+ # Initial definition
+ #
+ get "foo" do
+ :hi
+ end
+
+ ##
+ # YAY OVERRIDE
+ #
+ get "foo" do
+ :ho
+ end
+ APP
+
+ make_a_parser_for app
+ extract_routes
+
+ @routes.size.must_equal 1
+
+ r = @routes.first
+ r.name.must_equal 'GET "foo"'
+ r.comment.text.must_equal "YAY OVERRIDE"
+ end
+
+# May end up not doing this at all.
+# def test_get_definitions_automatically_add_a_head_definition_with_same_comment
+# flunk
+# end
+
+ def test_allows_normal_ruby_docs_to_be_mixed_in_same_doc
+ app = <<-APP
+ require "sinatra"
+
+ ##
+ # Route def
+ #
+ get "foo" do
+ :hi
+ end
+
+ #
+ # Random method
+ #
+ def foo; end
+
+ #
+ # Random class
+ #
+ class Yay
+
+ ##
+ # Random metamethod
+ #
+ some_meta :metafoo
+
+ #
+ # Random method in a class
+ #
+ def yayfoo; end
+ end
+ APP
+
+ make_a_parser_for app
+ extract_routes
+
+ @routes.size.must_equal 1
+
+ o = RDoc::TopLevel.find_class_named "Object"
+ o.method_list.first.name.must_equal "foo"
+
+ y = RDoc::TopLevel.find_class_named "Yay"
+ y.method_list.size.must_equal 2
+
+ y.method_list.shift.name.must_equal "metafoo"
+ y.method_list.shift.name.must_equal "yayfoo"
+ end
+
+ def test_parses_route_inside_a_class_definition
+ app = <<-APP
+ require "sinatra"
+
+ class Yay < Sinatra::Base
+ ##
+ # Route 9
+ #
+ get "foo" do
+ :hi
+ end
+ end
+ APP
+
+ make_a_parser_for app
+ extract_routes
+
+ @routes.size.must_equal 1
+
+ r = @routes.first
+ r.name.must_equal 'GET "foo"'
+ end
+
+# def test_parses_route_inside_a_sinatra_base_inheriting_class_only
+# flunk
+# end
+#
+# def test_parses_route_inside_a_base_class_into_that_class_not_application_routes
+# flunk
+# end
+
+
+ private
+
+ def make_a_parser_for(content)
+ @parser = RDoc::Parser::Sinatra.new @top_level, @filename, content, @options, @stats
+ end
+
+ def extract_routes
+ @parser.scan
+ @route_definitions = RDoc::TopLevel.find_module_named "Application Routes"
+ @routes = @route_definitions.method_list
+ end
+
+ def define_all_the_things!
+ <<-END
+require "sinatra"
+
+##
+# GET route
+#
+get "/foo" do
+ "hi"
+end
+
+##
+# HEAD route
+#
+head "/bar" do
+ "ho"
+end
+
+##
+# POST route
+#
+post "/foo/:id" do
+ :yay
+end
+
+##
+# PUT route
+#
+put "/foo/:id" do
+ :yay
+end
+
+##
+# DELETE route
+#
+delete %r{hi there} do
+ :ugg
+end
+
+##
+# PATCH route
+#
+patch /foo^bar/ do
+ :mug
+end
+
+##
+# OPTIONS route
+#
+options "foo/" do
+ :whatevs
+end
+ END
+ end
+end

0 comments on commit d26a824

Please sign in to comment.