From 1acd635c1a70990b2aca4cba20954fda32e71fee Mon Sep 17 00:00:00 2001 From: PavelPo99 Date: Sun, 24 Aug 2025 17:55:38 +0300 Subject: [PATCH] complete lesson 20 --- app/controllers/tests_controller.rb | 8 ++--- app/models/test.rb | 1 - config.ru | 3 ++ config/routes.rb | 1 + lib/middleware/logger.rb | 55 +++++++++++++++++++++++++++++ lib/simpler.rb | 2 -- lib/simpler/application.rb | 9 +++-- lib/simpler/controller.rb | 45 ++++++++++++++++++----- lib/simpler/router.rb | 2 -- lib/simpler/router/route.rb | 17 +++++++-- lib/simpler/view.rb | 4 +-- 11 files changed, 121 insertions(+), 26 deletions(-) create mode 100644 lib/middleware/logger.rb diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb index 1526a689..3a578689 100644 --- a/app/controllers/tests_controller.rb +++ b/app/controllers/tests_controller.rb @@ -1,11 +1,9 @@ class TestsController < Simpler::Controller - def index - @time = Time.now + render plain: 'Plain text response', status: 201, headers: { 'X-Custom' => 'Value' } end - def create - + def show + render plain: "Show test #{params[:id]}" end - end diff --git a/app/models/test.rb b/app/models/test.rb index 86376668..98cea0cb 100644 --- a/app/models/test.rb +++ b/app/models/test.rb @@ -4,5 +4,4 @@ # Integer :level, default: 0 # end class Test < Sequel::Model - end diff --git a/config.ru b/config.ru index 3060cc20..32d0368d 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,6 @@ require_relative 'config/environment' +require_relative 'lib/middleware/logger' +use LoggerForSimpler +use Rack::ContentType run Simpler.application diff --git a/config/routes.rb b/config/routes.rb index 4a751251..1700ff3a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Simpler.application.routes do get '/tests', 'tests#index' post '/tests', 'tests#create' + get '/tests/:id', 'tests#show' end diff --git a/lib/middleware/logger.rb b/lib/middleware/logger.rb new file mode 100644 index 00000000..d8096930 --- /dev/null +++ b/lib/middleware/logger.rb @@ -0,0 +1,55 @@ +require 'logger' + +class LoggerForSimpler + def initialize(app) + @logger = create_logger + @app = app + end + + def call(env) + request = Rack::Request.new(env) + status, headers, body = @app.call(env) + @logger.info(log_message(request, status, headers, env)) + + [status, headers, body] + end + + private + + def create_logger + log_dir = Simpler.root.join('log') + FileUtils.mkdir_p(log_dir) unless File.directory?(log_dir) + + log_file = File.expand_path(Simpler.root.join('log/app.log'), __dir__) || STROUT + Logger.new(log_file) + end + + def log_message(request, status, headers, env) + <<~LOG + Request: #{request.request_method} #{request.fullpath} + Handler: #{controller(env)}##{action(env)} + Parameters: #{request.params.inspect} + Response: #{status} [#{content_type(headers)}] #{template(env)} + LOG + end + + def controller(env) + env['simpler.controller'] ? env['simpler.controller'].class.name : 'UnknownController' + end + + def action(env) + env['simpler.action'] || 'unknown_action' + end + + def template(env) + return "#{env['simpler.template']}.html.erb" if env['simpler.template'] + + controller_name = env['simpler.controller']&.name || 'unknown_controller' + action = env['simpler.action'] || 'unknown_action' + "#{controller_name}/#{action}.html.erb" + end + + def content_type(headers) + headers['Content-Type'] || 'unknown_content_type' + end +end diff --git a/lib/simpler.rb b/lib/simpler.rb index f9dfe3c4..d4d365c9 100644 --- a/lib/simpler.rb +++ b/lib/simpler.rb @@ -2,7 +2,6 @@ require_relative 'simpler/application' module Simpler - class << self def application Application.instance @@ -12,5 +11,4 @@ def root Pathname.new(File.expand_path('..', __dir__)) end end - end diff --git a/lib/simpler/application.rb b/lib/simpler/application.rb index 711946a9..e9396d6f 100644 --- a/lib/simpler/application.rb +++ b/lib/simpler/application.rb @@ -6,7 +6,6 @@ module Simpler class Application - include Singleton attr_reader :db @@ -28,6 +27,9 @@ def routes(&block) def call(env) route = @router.route_for(env) + return not_found unless route + + env['simpler.route_params'] = route.extract_params(env['PATH_INFO']) || {} controller = route.controller.new(env) action = route.action @@ -37,7 +39,7 @@ def call(env) private def require_app - Dir["#{Simpler.root}/app/**/*.rb"].each { |file| require file } + Dir["#{Simpler.root}/app/**/*.rb"].sort.each { |file| require file } end def require_routes @@ -54,5 +56,8 @@ def make_response(controller, action) controller.make_response(action) end + def not_found + [404, {}, ['Not Found']] + end end end diff --git a/lib/simpler/controller.rb b/lib/simpler/controller.rb index 9383b035..9facd19f 100644 --- a/lib/simpler/controller.rb +++ b/lib/simpler/controller.rb @@ -2,38 +2,41 @@ module Simpler class Controller - attr_reader :name, :request, :response def initialize(env) @name = extract_name @request = Rack::Request.new(env) @response = Rack::Response.new + @render_options = {} end def make_response(action) @request.env['simpler.controller'] = self @request.env['simpler.action'] = action - set_default_headers send(action) write_response @response.finish end + def params + @request.params.merge(@request.env['simpler.route_params']) + end + private def extract_name self.class.name.match('(?.+)Controller')[:name].downcase end - def set_default_headers - @response['Content-Type'] = 'text/html' - end - def write_response - body = render_body + body = if @render_options.any? + render_from_options + else + render_body + end @response.write(body) end @@ -46,9 +49,33 @@ def params @request.params end - def render(template) - @request.env['simpler.template'] = template + def render(options) + if options.is_a?(Hash) + @render_options = options + else + @request.env['simpler.template'] = options + end end + def render_from_options + options = @render_options + + @response.status = options[:status] if options[:status] + + options[:headers]&.each { |key, value| @response[key] = value } + + if options.key?(:plain) + @response['Content-Type'] = 'text/plain' + options[:plain] + elsif options.key?(:json) + @response['Content-Type'] = 'application/json' + options[:json].to_json + elsif options.key?(:html) + @response['Content-Type'] = 'text/html' + options[:html] + else + '' + end + end end end diff --git a/lib/simpler/router.rb b/lib/simpler/router.rb index 14b3415c..b05449b3 100644 --- a/lib/simpler/router.rb +++ b/lib/simpler/router.rb @@ -2,7 +2,6 @@ module Simpler class Router - def initialize @routes = [] end @@ -36,6 +35,5 @@ def add_route(method, path, route_point) def controller_from_string(controller_name) Object.const_get("#{controller_name.capitalize}Controller") end - end end diff --git a/lib/simpler/router/route.rb b/lib/simpler/router/route.rb index 4c66b4b7..433fd502 100644 --- a/lib/simpler/router/route.rb +++ b/lib/simpler/router/route.rb @@ -1,7 +1,6 @@ module Simpler class Router class Route - attr_reader :controller, :action def initialize(method, path, controller, action) @@ -9,12 +8,26 @@ def initialize(method, path, controller, action) @path = path @controller = controller @action = action + @pattern = compile_path_pattern(path) end def match?(method, path) - @method == method && path.match(@path) + @method == method && path.match(@pattern) + end + + def extract_params(path) + match = path.match(@pattern) + match&.named_captures&.transform_keys(&:to_sym) end + private + + def compile_path_pattern(path) + segments = path.split('/').map do |segment| + segment.start_with?(':') ? "(?<#{segment[1..]}>[^\/]+)" : segment + end + Regexp.new("^#{segments.join('/')}$") + end end end end diff --git a/lib/simpler/view.rb b/lib/simpler/view.rb index 19a73b34..084ea771 100644 --- a/lib/simpler/view.rb +++ b/lib/simpler/view.rb @@ -2,8 +2,7 @@ module Simpler class View - - VIEW_BASE_PATH = 'app/views'.freeze + VIEW_BASE_PATH = 'app/views' def initialize(env) @env = env @@ -34,6 +33,5 @@ def template_path Simpler.root.join(VIEW_BASE_PATH, "#{path}.html.erb") end - end end