Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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...
commit d26a8243336e08f91acf137bdd1475718e475032 1 parent 82755c7
Eero Saynatkari rue authored
7 lib/rdoc/discover.rb
... ... @@ -0,0 +1,7 @@
  1 +begin
  2 + gem "rdoc", "~> 3"
  3 + require "rdoc/parser/sinatra"
  4 +
  5 +rescue Gem::LoadError
  6 + # Meh
  7 +end
148 lib/rdoc/parser/sinatra.rb
... ... @@ -0,0 +1,148 @@
  1 +# -*- encoding: utf-8 -*-
  2 +
  3 +require "rubygems"
  4 +
  5 +gem "rdoc", "~> 3"
  6 + require "rdoc"
  7 + require "rdoc/parser/ruby"
  8 +
  9 +
  10 +#
  11 +# Artificial scope for top-level routes.
  12 +#
  13 +class RDoc::SinatraRoutes < RDoc::AnonClass#RDoc::NormalModule
  14 +end
  15 +
  16 +#
  17 +# Sinatra route definition as a method.
  18 +#
  19 +class RDoc::SinatraRoute < RDoc::AnyMethod
  20 +
  21 + def initialize(route_definition, content_text)
  22 + super(content_text, route_definition)
  23 +
  24 + @params = ""
  25 + end
  26 +
  27 + def aref_prefix
  28 + ""
  29 + end
  30 +end
  31 +
  32 +#
  33 +# Sinatra routing error definition as a method.
  34 +#
  35 +class RDoc::SinatraRouteError < RDoc::SinatraRoute
  36 +end
  37 +
  38 +
  39 +#
  40 +# An augmented Ruby parser for Sinatra projects.
  41 +#
  42 +# In addition to normal Ruby doc, documentation is also extracted
  43 +# for route definitions.
  44 +#
  45 +class RDoc::Parser::Sinatra < RDoc::Parser::Ruby
  46 +
  47 + # Re-declare to force overriding the normal .rb handler.
  48 + parse_files_matching /\.rbw?$/i
  49 +
  50 +
  51 + HTTP_VERBS = %w[GET HEAD POST PUT PATCH DELETE OPTIONS]
  52 + HTTP_ERRORS = {"NOT_FOUND" => 404, "ERROR" => 500}
  53 +
  54 +
  55 + #
  56 + # New parser, adds a top-level Application Routes.
  57 + #
  58 + def initialize(top_level, file_name, content, options, stats)
  59 + super
  60 +
  61 + # Tuck away our little special module
  62 + @routes = @top_level.add_module RDoc::SinatraRoutes, "Application Routes"
  63 +
  64 + @current_route = nil
  65 + end
  66 +
  67 + #
  68 + # Override normal meta-method parsing to handle Sinatra routes and errors.
  69 + #
  70 + def parse_meta_method(container, single, token, comment)
  71 + name = token.name.upcase
  72 +
  73 + case name
  74 + when *HTTP_VERBS
  75 + r = parse_route_definition token
  76 + r.comment = comment
  77 + when "NOT_FOUND", "ERROR"
  78 + r = parse_error_definition token
  79 + r.comment = comment
  80 + else
  81 + super
  82 + end
  83 + end
  84 +
  85 +
  86 + private
  87 +
  88 + def parse_route_definition(http_method_token)
  89 + start_collecting_tokens
  90 + add_token http_method_token
  91 +
  92 + token_listener(self) {
  93 + skip_tkspace false
  94 +
  95 + pattern_token = get_tk
  96 + route_pattern = pattern_token.text
  97 +
  98 + route_name = "#{http_method_token.name.upcase} #{route_pattern}"
  99 +
  100 + if r = @routes.find_instance_method_named(route_name)
  101 + warn "Redefining route #{route_name}"
  102 + @current_route = r
  103 + else
  104 + @current_route = RDoc::SinatraRoute.new route_name, tokens_to_s
  105 + @routes.add_method @current_route
  106 + @stats.add_method @current_route
  107 + end
  108 + }
  109 +
  110 + @current_route
  111 + end
  112 +
  113 + def parse_error_definition(error_token)
  114 + start_collecting_tokens
  115 + add_token error_token
  116 +
  117 + token_listener(self) {
  118 + skip_tkspace false
  119 +
  120 + pattern_token = get_tk
  121 +
  122 + status_codes = if TkDO === pattern_token
  123 + HTTP_ERRORS["ERROR"]
  124 + else
  125 + pattern_token.text
  126 + end
  127 +
  128 + if error_token.name == "not_found"
  129 + route_name = "error #{HTTP_ERRORS["NOT_FOUND"]}"
  130 + else
  131 + route_name = "error #{status_codes}"
  132 + end
  133 +
  134 + if r = @routes.find_instance_method_named(route_name)
  135 + warn "Redefining error #{error_token.name} #{pattern_token}"
  136 + @current_route = r
  137 + else
  138 + @current_route = RDoc::SinatraRouteError.new route_name, tokens_to_s
  139 + @routes.add_method @current_route
  140 + @stats.add_method @current_route
  141 + end
  142 + }
  143 +
  144 + @current_route
  145 + end
  146 +
  147 +
  148 +end
420 test/test_rdoc_parser_sinatra.rb
... ... @@ -0,0 +1,420 @@
  1 +# -*- encoding: utf-8 -*-
  2 +
  3 +require "stringio"
  4 +require "tempfile"
  5 +
  6 +require "rubygems"
  7 +
  8 +gem "rdoc", "~> 3"
  9 + require "rdoc"
  10 + require "rdoc/parser/sinatra"
  11 +
  12 +gem "minitest"
  13 + require "minitest/autorun"
  14 +
  15 +
  16 +class TestRDocParserSinatra < MiniTest::Unit::TestCase
  17 +
  18 + def setup
  19 + @tempfile = Tempfile.new self.class.name
  20 + @filename = @tempfile.path
  21 +
  22 + RDoc::TopLevel.reset
  23 + @top_level = RDoc::TopLevel.new @filename
  24 +
  25 + @options = RDoc::Options.new
  26 + @options.quiet = true
  27 + @stats = RDoc::Stats.new 0
  28 +
  29 + @route_definitions = @routes = nil
  30 + end
  31 +
  32 + def teardown
  33 + @tempfile.close
  34 + end
  35 +
  36 +
  37 +
  38 + def test_routes_stored_in_top_level_application_routes_class_hiding_in_modules
  39 + app = <<-APP
  40 + require "sinatra"
  41 +
  42 + ##
  43 + # Route 1
  44 + #
  45 + get "/foo" do
  46 + :hi
  47 + end
  48 + APP
  49 +
  50 + make_a_parser_for app
  51 + @parser.scan
  52 +
  53 + r = RDoc::TopLevel.find_module_named "Application Routes"
  54 + r.wont_be_nil
  55 +
  56 + r.method_list.first.name.must_equal 'GET "/foo"'
  57 + end
  58 +
  59 + def test_route_presented_as_http_method_and_route_pattern
  60 + app = <<-APP
  61 + require "sinatra"
  62 +
  63 + ##
  64 + # Route 2
  65 + #
  66 + # Some more text goes here.
  67 + #
  68 + get "/foo" do
  69 + :hi
  70 + end
  71 + APP
  72 +
  73 + make_a_parser_for app
  74 + extract_routes
  75 +
  76 + r = @routes.first
  77 +
  78 + r.name.must_equal 'GET "/foo"'
  79 + r.comment.text.must_equal "Route 2\n\nSome more text goes here."
  80 + end
  81 +
  82 + def test_route_pattern_may_be_regexp
  83 + app = <<-APP
  84 + require "sinatra"
  85 +
  86 + ##
  87 + # Route 3
  88 + #
  89 + get /foo/ do
  90 + :hi
  91 + end
  92 +
  93 + ##
  94 + # Route 4
  95 + #
  96 + get %r{bar} do
  97 + :ho
  98 + end
  99 + APP
  100 +
  101 + make_a_parser_for app
  102 + extract_routes
  103 +
  104 + @routes.size.must_equal 2
  105 +
  106 + r = @routes.first
  107 + r.name.must_equal 'GET /foo/'
  108 + r.comment.text.must_equal "Route 3"
  109 +
  110 + r = @routes.last
  111 + r.name.must_equal 'GET %r{bar}'
  112 + r.comment.text.must_equal "Route 4"
  113 + end
  114 +
  115 +
  116 + %w[GET HEAD POST PUT PATCH DELETE OPTIONS].each {|http|
  117 + define_method("test_parses_#{http}_definition") {
  118 + make_a_parser_for define_all_the_things!
  119 + extract_routes
  120 +
  121 + @routes.find {|r| r.name.include? http }.wont_be_nil
  122 + }
  123 + }
  124 +
  125 + def test_parses_error_definitions_without_status_code_defaulting_to_500
  126 + app = <<-APP
  127 + require "sinatra"
  128 +
  129 + ##
  130 + # Gettersy
  131 + #
  132 + get "foo" do
  133 + :hi
  134 + end
  135 +
  136 + ##
  137 + # OMG ERROR
  138 + #
  139 + error do
  140 + :haha
  141 + end
  142 + APP
  143 +
  144 + make_a_parser_for app
  145 + extract_routes
  146 +
  147 + @routes.size.must_equal 2
  148 +
  149 + r = @routes.last
  150 + r.name.must_equal "error 500"
  151 + end
  152 +
  153 + def test_parses_error_definitions_with_status_codes
  154 + app = <<-APP
  155 + require "sinatra"
  156 +
  157 + ##
  158 + # Getters
  159 + #
  160 + get "foo" do
  161 + :hi
  162 + end
  163 +
  164 + ##
  165 + # Fourohthree
  166 + #
  167 + error 403 do
  168 + :haha
  169 + end
  170 + APP
  171 +
  172 + make_a_parser_for app
  173 + extract_routes
  174 +
  175 + @routes.size.must_equal 2
  176 +
  177 + r = @routes.last
  178 + r.name.must_equal "error 403"
  179 + end
  180 +
  181 + def test_parses_not_found_definition_into_a_404_error
  182 + app = <<-APP
  183 + require "sinatra"
  184 +
  185 + ##
  186 + # Route 5
  187 + #
  188 + get "foo" do
  189 + :hi
  190 + end
  191 +
  192 + ##
  193 + # Route 6
  194 + #
  195 + not_found do
  196 + :haha
  197 + end
  198 + APP
  199 +
  200 + make_a_parser_for app
  201 + extract_routes
  202 +
  203 + @routes.size.must_equal 2
  204 +
  205 + r = @routes.last
  206 + r.name.must_equal "error 404"
  207 + end
  208 +
  209 + def test_parses_same_route_pattern_with_different_methods_as_separate
  210 + app = <<-APP
  211 + require "sinatra"
  212 +
  213 + ##
  214 + # Route 7
  215 + #
  216 + get "foo" do
  217 + :hi
  218 + end
  219 +
  220 + ##
  221 + # Route 8
  222 + #
  223 + put "foo" do
  224 + :ho
  225 + end
  226 + APP
  227 +
  228 + make_a_parser_for app
  229 + extract_routes
  230 +
  231 + @routes.size.must_equal 2
  232 +
  233 + r = @routes.first
  234 + r.name.must_equal 'GET "foo"'
  235 +
  236 + r = @routes.last
  237 + r.name.must_equal 'PUT "foo"'
  238 + end
  239 +
  240 + def test_parses_same_route_pattern_with_same_method_as_the_same_and_latter_overrides
  241 + app = <<-APP
  242 + require "sinatra"
  243 +
  244 + ##
  245 + # Initial definition
  246 + #
  247 + get "foo" do
  248 + :hi
  249 + end
  250 +
  251 + ##
  252 + # YAY OVERRIDE
  253 + #
  254 + get "foo" do
  255 + :ho
  256 + end
  257 + APP
  258 +
  259 + make_a_parser_for app
  260 + extract_routes
  261 +
  262 + @routes.size.must_equal 1
  263 +
  264 + r = @routes.first
  265 + r.name.must_equal 'GET "foo"'
  266 + r.comment.text.must_equal "YAY OVERRIDE"
  267 + end
  268 +
  269 +# May end up not doing this at all.
  270 +# def test_get_definitions_automatically_add_a_head_definition_with_same_comment
  271 +# flunk
  272 +# end
  273 +
  274 + def test_allows_normal_ruby_docs_to_be_mixed_in_same_doc
  275 + app = <<-APP
  276 + require "sinatra"
  277 +
  278 + ##
  279 + # Route def
  280 + #
  281 + get "foo" do
  282 + :hi
  283 + end
  284 +
  285 + #
  286 + # Random method
  287 + #
  288 + def foo; end
  289 +
  290 + #
  291 + # Random class
  292 + #
  293 + class Yay
  294 +
  295 + ##
  296 + # Random metamethod
  297 + #
  298 + some_meta :metafoo
  299 +
  300 + #
  301 + # Random method in a class
  302 + #
  303 + def yayfoo; end
  304 + end
  305 + APP
  306 +
  307 + make_a_parser_for app
  308 + extract_routes
  309 +
  310 + @routes.size.must_equal 1
  311 +
  312 + o = RDoc::TopLevel.find_class_named "Object"
  313 + o.method_list.first.name.must_equal "foo"
  314 +
  315 + y = RDoc::TopLevel.find_class_named "Yay"
  316 + y.method_list.size.must_equal 2
  317 +
  318 + y.method_list.shift.name.must_equal "metafoo"
  319 + y.method_list.shift.name.must_equal "yayfoo"
  320 + end
  321 +
  322 + def test_parses_route_inside_a_class_definition
  323 + app = <<-APP
  324 + require "sinatra"
  325 +
  326 + class Yay < Sinatra::Base
  327 + ##
  328 + # Route 9
  329 + #
  330 + get "foo" do
  331 + :hi
  332 + end
  333 + end
  334 + APP
  335 +
  336 + make_a_parser_for app
  337 + extract_routes
  338 +
  339 + @routes.size.must_equal 1
  340 +
  341 + r = @routes.first
  342 + r.name.must_equal 'GET "foo"'
  343 + end
  344 +
  345 +# def test_parses_route_inside_a_sinatra_base_inheriting_class_only
  346 +# flunk
  347 +# end
  348 +#
  349 +# def test_parses_route_inside_a_base_class_into_that_class_not_application_routes
  350 +# flunk
  351 +# end
  352 +
  353 +
  354 + private
  355 +
  356 + def make_a_parser_for(content)
  357 + @parser = RDoc::Parser::Sinatra.new @top_level, @filename, content, @options, @stats
  358 + end
  359 +
  360 + def extract_routes
  361 + @parser.scan
  362 + @route_definitions = RDoc::TopLevel.find_module_named "Application Routes"
  363 + @routes = @route_definitions.method_list
  364 + end
  365 +
  366 + def define_all_the_things!
  367 + <<-END
  368 +require "sinatra"
  369 +
  370 +##
  371 +# GET route
  372 +#
  373 +get "/foo" do
  374 + "hi"
  375 +end
  376 +
  377 +##
  378 +# HEAD route
  379 +#
  380 +head "/bar" do
  381 + "ho"
  382 +end
  383 +
  384 +##
  385 +# POST route
  386 +#
  387 +post "/foo/:id" do
  388 + :yay
  389 +end
  390 +
  391 +##
  392 +# PUT route
  393 +#
  394 +put "/foo/:id" do
  395 + :yay
  396 +end
  397 +
  398 +##
  399 +# DELETE route
  400 +#
  401 +delete %r{hi there} do
  402 + :ugg
  403 +end
  404 +
  405 +##
  406 +# PATCH route
  407 +#
  408 +patch /foo^bar/ do
  409 + :mug
  410 +end
  411 +
  412 +##
  413 +# OPTIONS route
  414 +#
  415 +options "foo/" do
  416 + :whatevs
  417 +end
  418 + END
  419 + end
  420 +end

0 comments on commit d26a824

Please sign in to comment.
Something went wrong with that request. Please try again.