├── README.md ├── app ├── controllers │ └── posts_controller.rb └── views │ └── posts │ ├── index.html.erb │ └── show.html.erb ├── config └── routes.rb ├── database.yml ├── internal ├── fake_rails.rb └── fake_rails │ ├── application.rb │ ├── controller.rb │ ├── dispatcher.rb │ └── routes.rb └── server.rb /README.md: -------------------------------------------------------------------------------- 1 | Just having fun trying to recreate basic Rails functionality with pure ruby, no gems. 2 | 3 | Inspiration and a lot of code in this repository comes from @OngMaple's amazing talk on Building web apps with Ruby Standard Library, at Euruko 2021 (https://www.youtube.com/watch?v=lxczDssLYKA) and @pedrogaspar's follow up on creating a TCP chat server (https://twitter.com/pedrogaspar/status/1398798494169772032). 4 | 5 | To start the server, simply run `ruby server.rb`. 6 | 7 | **Index page** 8 | 9 |  10 | 11 | **Show page** 12 | 13 |  14 | -------------------------------------------------------------------------------- /app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | require "./internal/fake_rails/controller.rb" 2 | 3 | class PostsController < FakeRails::Controller 4 | def index 5 | @posts = database.transaction { database[:posts] } 6 | 7 | @posts = @posts.map {|p| OpenStruct.new(p)} 8 | end 9 | 10 | def show 11 | @post = database.transaction { OpenStruct.new(database[:posts][params[:id].to_i]) } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/posts/index.html.erb: -------------------------------------------------------------------------------- 1 |
<%= post.content %>
6 |<%= @post.content %>
3 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | FakeRails.application.routes.draw do 2 | resources :posts 3 | end 4 | -------------------------------------------------------------------------------- /database.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :posts: 3 | - :title: Amazing title 4 | :content: Here's the content 5 | - :title: second post 6 | :content: Another content! 7 | -------------------------------------------------------------------------------- /internal/fake_rails.rb: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "uri" 3 | require "yaml/store" 4 | require "erb" 5 | require "ostruct" 6 | 7 | require "./internal/fake_rails/dispatcher.rb" 8 | require "./internal/fake_rails/application.rb" 9 | require "./internal/fake_rails/routes.rb" 10 | require "./app/controllers/posts_controller.rb" 11 | 12 | module FakeRails 13 | def self.application 14 | @application ||= Application.new 15 | end 16 | end 17 | 18 | require "./config/routes.rb" 19 | -------------------------------------------------------------------------------- /internal/fake_rails/application.rb: -------------------------------------------------------------------------------- 1 | module FakeRails 2 | class Application 3 | attr_accessor :routes 4 | 5 | def initialize 6 | @routes = Routes.new 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /internal/fake_rails/controller.rb: -------------------------------------------------------------------------------- 1 | module FakeRails 2 | class Controller 3 | attr_reader :database, :params 4 | 5 | def initialize(params:) 6 | @database = YAML::Store.new("database.yml") 7 | @params = params 8 | end 9 | 10 | def get_binding 11 | binding 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /internal/fake_rails/dispatcher.rb: -------------------------------------------------------------------------------- 1 | require "./internal/fake_rails.rb" 2 | 3 | module FakeRails 4 | class Dispatcher 5 | attr_accessor :params 6 | 7 | def initialize(request_line:) 8 | @request_line = request_line 9 | @params = {} 10 | end 11 | 12 | def http_response 13 | if current_path 14 | status_code = "200 OK" 15 | 16 | controller = Kernel.const_get("#{controller_name.capitalize}Controller").new(params: params) 17 | controller.send(controller_action) 18 | 19 | body = erb_view.result(controller.get_binding) 20 | else 21 | status_code = "404 NOT FOUND" 22 | body = "Didn't hit any endpoint" 23 | end 24 | 25 | puts status_code 26 | 27 | <<~MSG 28 | HTTP/1.1 #{status_code} 29 | Content-Type: text/html 30 | location: /show/data 31 | 32 | #{body} 33 | MSG 34 | end 35 | 36 | private 37 | 38 | def controller_name 39 | current_path[:controller_name] 40 | end 41 | 42 | def controller_action 43 | current_path[:controller_action] 44 | end 45 | 46 | def current_path 47 | return @current_path if @current_path 48 | 49 | @current_path = paths.find { |path| path[:method] == request_method && path[:url] == request_url_with_params } 50 | end 51 | 52 | def paths 53 | FakeRails.application.routes.paths 54 | end 55 | 56 | def request_method 57 | @request_line.split[0] 58 | end 59 | 60 | def request_url_with_params 61 | return @request_url_with_params if @request_url_with_params 62 | 63 | # update params to correctly match paths with keywords, ex. /posts/2 to /posts/:id 64 | url_parts = request_url.split("/") 65 | paths.each do |path| 66 | path_url_parts = path[:url].split("/") 67 | path_url_parts.each.with_index do |part, index| 68 | if part.start_with?(":") && url_parts[index] 69 | params[part.sub(":", "").to_sym] = url_parts[index] 70 | url_parts[index] = part 71 | end 72 | end 73 | end 74 | 75 | @request_url_with_params = url_parts.join("/") 76 | end 77 | 78 | def request_url 79 | @request_url ||= @request_line.split[1] 80 | end 81 | 82 | def request_version_number 83 | @request_line.split[2] 84 | end 85 | 86 | def erb_view 87 | file_content = File.read("./app/views/#{controller_name}/#{controller_action}.html.erb") 88 | body = ERB.new(file_content) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /internal/fake_rails/routes.rb: -------------------------------------------------------------------------------- 1 | module FakeRails 2 | class Routes 3 | attr_accessor :paths 4 | 5 | def initialize 6 | @paths = [] 7 | end 8 | 9 | def draw(&block) 10 | instance_exec(&block) 11 | end 12 | 13 | def resources(name) 14 | @paths = [ 15 | {method: "GET", url: "/#{name}", controller_action: "index", controller_name: name}, 16 | {method: "GET", url: "/#{name}/:id", controller_action: "show", controller_name: name}, 17 | {method: "GET", url: "/#{name}/new", controller_action: "new", controller_name: name}, 18 | ] 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /server.rb: -------------------------------------------------------------------------------- 1 | require "./internal/fake_rails.rb" 2 | 3 | server = TCPServer.new(3000) 4 | 5 | loop do 6 | client = server.accept 7 | 8 | request_line = client.readline 9 | 10 | http_response = FakeRails::Dispatcher.new(request_line: request_line).http_response 11 | 12 | client.puts(http_response) 13 | 14 | client.close 15 | end 16 | --------------------------------------------------------------------------------