├── mini-rails.rb ├── readme.md └── test.rb /mini-rails.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'thread' 3 | 4 | module WEBrick 5 | 6 | module Utils 7 | #https://github.com/candlerb/webrick/blob/master/lib/webrick/utils.rb 8 | def create_listeners 9 | address='0.0.0.0' 10 | port=8081 11 | res = Socket::getaddrinfo(address, port, Socket::AF_UNSPEC, Socket::SOCK_STREAM, 1, Socket::AI_PASSIVE) 12 | sockets = [] 13 | res.each{|ai| 14 | puts ("TCPServer.new(#{ai[3]}, #{port})") 15 | sock = TCPServer.new(ai[3], port) 16 | sockets << sock 17 | } 18 | sockets 19 | end 20 | module_function :create_listeners 21 | end 22 | 23 | #https://github.com/candlerb/webrick/blob/master/lib/webrick/server.rb 24 | class SimpleServer 25 | def SimpleServer.start 26 | yield 27 | end 28 | end 29 | 30 | #https://github.com/candlerb/webrick/blob/master/lib/webrick/server.rb 31 | class GenericServer 32 | def start 33 | @listeners = Utils::create_listeners 34 | SimpleServer.start{ 35 | while true 36 | svrs = IO.select(@listeners, nil, nil, 2.0) 37 | if svrs 38 | svrs[0].each{|svr| 39 | sock = svr.accept 40 | sock.sync = true 41 | start_thread sock if sock 42 | } 43 | end 44 | end 45 | } 46 | end 47 | def run(sock) 48 | raise 'run() must be provided by user.' 49 | end 50 | def start_thread(sock) 51 | Thread.start{ 52 | begin 53 | run sock 54 | ensure 55 | sock.close 56 | end 57 | } 58 | end 59 | end 60 | 61 | #https://github.com/candlerb/webrick/blob/master/lib/webrick/httprequest.rb 62 | class HTTPRequest 63 | LF="\n" 64 | def parse(socket=nil) 65 | @request = socket.gets LF,4096 66 | puts @request 67 | @path_info = @request 68 | end 69 | def path 70 | '/' 71 | end 72 | def meta_vars 73 | meta = Hash.new 74 | meta["PATH_INFO"] = @path_info 75 | meta 76 | end 77 | end 78 | 79 | #https://github.com/candlerb/webrick/blob/master/lib/webrick/httpresponse.rb 80 | class HTTPResponse 81 | attr_reader :header, :body 82 | def initialize 83 | @header=Hash.new 84 | @body=[] 85 | end 86 | def send_response(sock) 87 | sock << "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n" 88 | @body.each { |part| sock << part } 89 | end 90 | end 91 | 92 | #https://github.com/candlerb/webrick/blob/master/lib/webrick/httpserver.rb 93 | class HTTPServer < GenericServer 94 | def initialize 95 | @mount_tab = Hash.new 96 | end 97 | def run(sock) 98 | req=HTTPRequest.new 99 | res=HTTPResponse.new 100 | req.parse sock 101 | self.service(req, res) 102 | res.send_response(sock) 103 | sock.close 104 | end 105 | def service(req, res) 106 | servlet, options = search_servlet req.path 107 | si = servlet.get_instance self, *options 108 | si.service req, res 109 | end 110 | def search_servlet(path) 111 | @mount_tab[path] 112 | end 113 | def mount(dir,servlet,*options) 114 | @mount_tab[dir] = [servlet,options] 115 | end 116 | end 117 | 118 | end 119 | 120 | #WEBrick::HTTPServer.new.start 121 | 122 | module WEBrick 123 | module HTTPServlet 124 | 125 | #https://github.com/candlerb/webrick/blob/master/lib/webrick/httpservlet/abstract.rb 126 | class AbstractServlet 127 | def initialize(server, *options) 128 | @server = server 129 | @options = options 130 | end 131 | def self.get_instance(server,*options) 132 | self.new server,*options 133 | end 134 | def service(req, res) 135 | raise 'service(req, res) must be provided by user.' 136 | end 137 | end 138 | 139 | end 140 | end 141 | 142 | module Rack 143 | module Handler 144 | 145 | def self.default 146 | Rack::Handler::WEBrick 147 | end 148 | 149 | #https://github.com/rack/rack/blob/master/lib/rack/handler/webrick.rb 150 | class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet 151 | def self.run(app) 152 | @server = ::WEBrick::HTTPServer.new 153 | @server.mount '/',Rack::Handler::WEBrick, app 154 | @server.start 155 | end 156 | def initialize(server, app) 157 | super server 158 | @app = app 159 | end 160 | def service(req, res) 161 | env = req.meta_vars 162 | status, headers, body = @app.call(env) 163 | body.each { |part| res.body << part } 164 | end 165 | end 166 | 167 | end 168 | 169 | #https://github.com/rack/rack/blob/master/lib/rack/server.rb 170 | class Server 171 | def self.start 172 | new.start 173 | end 174 | def start &blk 175 | server.run wrapped_app, &blk 176 | end 177 | def server 178 | @_server = Rack::Handler.default 179 | end 180 | def build_app_and_options_from_config 181 | app = Rack::Builder.parse_file 'config.ru' 182 | end 183 | def wrapped_app 184 | @wrapped_app ||= build_app app 185 | end 186 | def app 187 | @app ||= build_app_and_options_from_config 188 | end 189 | def build_app(app) 190 | middleware.reverse_each do |middleware| 191 | middleware = middleware.call(self) 192 | klass, *args = middleware 193 | app = klass.new(app, *args) 194 | end 195 | app 196 | end 197 | def middleware 198 | self.class.middleware 199 | end 200 | end 201 | 202 | #https://github.com/rack/rack/blob/master/lib/rack/builder.rb 203 | class Builder 204 | def self.parse_file(config) 205 | cfgfile='Rails.application' #read config.ru 206 | app=new_from_string cfgfile 207 | app 208 | end 209 | def self.new_from_string(builder_script) 210 | app = eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app" 211 | end 212 | def initialize(default_app = nil,&block) 213 | @run = default_app 214 | @run = instance_eval(&block) if block_given? 215 | end 216 | def to_app 217 | app = @run 218 | app 219 | end 220 | end 221 | 222 | class Request 223 | module Env 224 | attr_reader :env 225 | def initialize(env) 226 | @env=env 227 | super() 228 | end 229 | end 230 | end 231 | end 232 | 233 | #Rack::Server.start 234 | module Rails 235 | 236 | #https://github.com/rails/rails/blob/master/railties/lib/rails/commands/server/server_command.rb 237 | class Server < ::Rack::Server 238 | def initialize 239 | super 240 | end 241 | def start 242 | super 243 | end 244 | def middleware 245 | Hash.new([]) 246 | end 247 | end 248 | 249 | module Command 250 | 251 | #https://github.com/rails/rails/blob/56b3849316b9c4cf4423ef8de30cbdc1b7e0f7af/railties/lib/rails/command/actions.rb 252 | module Actions 253 | end 254 | class Base 255 | include Actions 256 | end 257 | 258 | #https://github.com/rails/rails/blob/master/railties/lib/rails/commands/server/server_command.rb 259 | class ServerCommand < Base 260 | def perform 261 | #require APP_PATH 262 | Rails::Server.new.tap do |server| 263 | server.start 264 | end 265 | end 266 | end 267 | 268 | #https://github.com/rails/rails/blob/master/railties/lib/rails/commands/application/application_command.rb 269 | class ApplicationCommand < Base 270 | def perform(*args) 271 | Rails::Command::ServerCommand.new.perform 272 | end 273 | end 274 | 275 | #https://github.com/rails/rails/blob/master/railties/lib/rails/command.rb 276 | class << self 277 | def invoke(namespace,args) 278 | command=find_by_namespace namespace 279 | command.perform namespace,args 280 | end 281 | def find_by_namespace(namespace) 282 | case namespace 283 | when :application 284 | Rails::Command::ApplicationCommand.new 285 | end 286 | end 287 | end 288 | 289 | end 290 | 291 | module AppLoader 292 | extend self 293 | def exec_app 294 | Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd)) 295 | end 296 | end 297 | end 298 | 299 | 300 | 301 | 302 | module Rails 303 | 304 | class << self 305 | #https://github.com/rails/rails/blob/master/railties/lib/rails.rb#L36 306 | @application = @app_class = nil 307 | attr_writer :application 308 | attr_accessor :app_class 309 | def application 310 | @application ||= (app_class.instance if app_class) 311 | end 312 | end 313 | 314 | #https://github.com/rails/rails/blob/master/railties/lib/rails/engine.rb 315 | class Engine 316 | def call(env) 317 | req = build_request env 318 | app.call req.env 319 | end 320 | def app 321 | @app ||= begin 322 | stack = default_middleware_stack 323 | config.middleware = config.middleware.merge_into stack 324 | config.middleware.build endpoint 325 | end 326 | #@app = Proc.new{|*args| ['200',[],["hello from rails engine."]] } 327 | end 328 | class <$2 || 'index',:controller=>$1 || 'Home'} 405 | end 406 | end 407 | end 408 | 409 | #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/request.rb 410 | class Request 411 | include Rack::Request::Env 412 | include ActionDispatch::Http::Parameters 413 | attr_accessor :controller_instance, :path_info 414 | def initialize(env) 415 | super 416 | end 417 | 418 | def controller_class 419 | params = path_parameters 420 | controller_param = params[:controller] 421 | params[:action] ||= "index" 422 | const_name = "#{controller_param}Controller" 423 | #ActiveSupport::Dependencies.constantize(const_name) 424 | eval const_name 425 | end 426 | end 427 | #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/response.rb 428 | class Response 429 | attr_accessor :request 430 | def self.create(status = 200, header = {}, body = []) 431 | new status, header, body 432 | end 433 | def initialize(status = 200, header = {}, body = []) 434 | end 435 | end 436 | 437 | # https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/stack.rb 438 | class MiddlewareStack 439 | class Middleware 440 | attr_reader :klass 441 | def initialize(klass,args,block) 442 | @klass=klass 443 | end 444 | def build(app,*args,&block) 445 | klass.new(app, *args, &block) 446 | end 447 | end 448 | 449 | attr_accessor :middlewares 450 | def initialize 451 | @middlewares= [] 452 | end 453 | def use(klass, *args, &block) 454 | middlewares.push(build_middleware(klass, args, block)) 455 | end 456 | def build_middleware(klass, args, block) 457 | Middleware.new(klass, args, block) 458 | end 459 | def build(app = Proc.new) 460 | #return Proc.new{|*args| ['200',[],["hello from rails MiddlewareStack."]] } 461 | builds=middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) } 462 | builds 463 | end 464 | end 465 | 466 | #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/executor.rb 467 | class Executor 468 | def initialize(app, executor=nil) 469 | @app, @executor = app, executor 470 | end 471 | def call(env) 472 | #return ['200',[],["hello from Executor."] ] 473 | #state = @executor.run! if @executor 474 | response = @app.call(env) 475 | end 476 | end 477 | 478 | module Routing 479 | #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/routing/endpoint.rb 480 | class Endpoint 481 | def app; self; end 482 | end 483 | 484 | #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/routing/route_set.rb 485 | class RouteSet 486 | class Dispatcher < Routing::Endpoint 487 | def serve(req) 488 | #return ['200',[],["hello from RouteSet Dispatcher."] ] 489 | begin 490 | params = req.path_parameters 491 | controller = controller req 492 | res = controller.make_response! req 493 | dispatch(controller, params[:action], req, res) 494 | rescue 495 | ['200',[],["404"] ] 496 | end 497 | end 498 | def dispatch(controller, action, req, res) 499 | controller.dispatch(action, req, res) 500 | end 501 | def controller(req) 502 | req.controller_class 503 | end 504 | end 505 | 506 | Config = Struct.new :relative_url_root, :api_only 507 | DEFAULT_CONFIG = Config.new(nil, false) 508 | def self.new_with_config(config) 509 | route_set_config = DEFAULT_CONFIG 510 | new route_set_config 511 | end 512 | def initialize(config = DEFAULT_CONFIG) 513 | @set = Journey::Routes.new 514 | @router = Journey::Router.new @set 515 | #Routes should be set by route mapping registers, hard code here for simplicity 516 | @set.instance_eval{ @routes << Dispatcher.new } 517 | end 518 | def call(env) 519 | req = make_request(env) 520 | req.path_info = Journey::Router::Utils.normalize_path(req.path_info) 521 | @router.serve(req) 522 | end 523 | def request_class 524 | ActionDispatch::Request 525 | end 526 | def make_request(env) 527 | request_class.new env 528 | end 529 | end 530 | end 531 | 532 | module Journey 533 | #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/journey/router.rb 534 | class Router 535 | attr_accessor :routes 536 | def initialize(routes) 537 | @routes = routes 538 | end 539 | def serve(req) 540 | #return ['200',[],["hello from Router."] ] 541 | find_routes(req).each do |match, parameters, route| 542 | status, headers, body = route.app.serve(req) 543 | return [status, headers, body] 544 | end 545 | end 546 | def find_routes(req) 547 | routes.map{|x|[nil,nil,x]} 548 | end 549 | class Utils 550 | def self.normalize_path(path) 551 | path = "/#{path}" 552 | path.squeeze!("/".freeze) 553 | path.sub!(%r{/+\Z}, "".freeze) 554 | path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } 555 | path = "/" if path == "".freeze 556 | path 557 | end 558 | end 559 | end 560 | 561 | #https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/journey/routes.rb 562 | class Routes 563 | include Enumerable 564 | attr_reader :routes 565 | def initialize 566 | @routes = [] 567 | end 568 | def each(&block) 569 | routes.each(&block) 570 | end 571 | end 572 | end 573 | end 574 | 575 | #https://github.com/rails/rails/blob/master/actionpack/lib/abstract_controller/base.rb 576 | module AbstractController 577 | class Base 578 | attr_accessor :action_name 579 | def process(action, *args) 580 | @action_name = action.to_s 581 | process_action(action_name, *args) 582 | end 583 | def process_action(method_name, *args) 584 | send_action(method_name, *args) 585 | end 586 | alias send_action send 587 | end 588 | end 589 | module ActionController 590 | class MiddlewareStack < ActionDispatch::MiddlewareStack 591 | end 592 | 593 | #https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal.rb 594 | class Metal < AbstractController::Base 595 | def initialize 596 | @_request = nil 597 | @_response = nil 598 | @_routes = nil 599 | super 600 | end 601 | def dispatch(name,request,response) 602 | #return ['200',[],["hello from rails Metal Controller."]] 603 | set_request!(request) 604 | set_response!(response) 605 | process(name) 606 | end 607 | def self.dispatch(name, req, res) 608 | new.dispatch name,req,res 609 | end 610 | def set_response!(response) # :nodoc: 611 | @_response = response 612 | end 613 | def set_request!(request) #:nodoc: 614 | @_request = request 615 | @_request.controller_instance = self 616 | end 617 | def self.make_response!(request) 618 | ActionDispatch::Response.create.tap do |res| res.request= request end 619 | end 620 | def self.call(env) 621 | req = ActionDispatch::Request.new env 622 | action(req.path_parameters[:action]).call(env) 623 | end 624 | def self.action(name) 625 | lambda { |env| 626 | req = ActionDispatch::Request.new(env) 627 | res = make_response! req 628 | new.dispatch(name, req, res) 629 | } 630 | end 631 | end 632 | #https://github.com/rails/rails/blob/master/actionpack/lib/action_co 633 | class Base < Metal 634 | end 635 | 636 | end 637 | 638 | 639 | def start_server 640 | Rails::AppLoader.exec_app 641 | Rails::Command.invoke :application, ARGV 642 | end 643 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | mini-rails 2 | ===================== 3 | RubyChina讨论贴:https://ruby-china.org/topics/32764 4 | 5 | ## What is mini-rails? 6 | 7 | 本想学习一下Rails源码,看看有什么magic;
8 | 但源码文件太多,核心代码都淹没在大量细节实现中,全部看完不现实,走马观花又很难领会精髓;
9 | 纸上得来终觉浅,眼过千遍,不如手过一遍,干脆重新造个轮子;
10 | 于是就有了mini-rails,参照Rails源码,省略细节,实现了一个self-host mvc框架;
11 | 再也不用对着庞大的Rails望洋兴叹了,600行代码为您还原一个真实的Rails;
12 | 13 | mini-rails实现了从socket到controller的层层封装,并注释了Rails源码中相应模块的位置,可作为学习Rails源码的目录或大纲;
14 | ``` 15 | Socket -> WEBrick GenericServer -> WEBrick HTTPServer -> WEBrick HTTPServlet 16 | -> Rack WEBrick -> Rack Handler -> Rack Server 17 | -> Rails Server -> Engine -> Application -> Middleware -> ActionDispatch::Executor 18 | -> MiddlewareStack -> Routing 19 | -> Journey Router -> AbstractController -> Metal -> ActionController::Base 20 | ``` 21 | 22 | ## How to run it? 23 | ```ruby 24 | #运行test.rb,打开浏览器: 25 | http://localhost:8081/Home/index => "hello from Home Index." 26 | http://localhost:8081/User/about => "hello from User About." 27 | http://localhost:8081/unkown_url => "404" 28 | ``` 29 | 30 | ## How to define a web page? 31 | ```ruby 32 | require_relative 'mini-rails' 33 | 34 | #define your controllers and actions 35 | class UserController < ActionController::Base 36 | def about 37 | ['200',[],["hello from User About."]] 38 | end 39 | end 40 | 41 | start_server 42 | 43 | ``` 44 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'mini-rails' 2 | 3 | class HomeController < ActionController::Base 4 | def index 5 | ['200',[],["hello from Home Index."]] 6 | end 7 | end 8 | 9 | class UserController < ActionController::Base 10 | def about 11 | ['200',[],["hello from User About."]] 12 | end 13 | end 14 | 15 | start_server 16 | --------------------------------------------------------------------------------