diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ba91883d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +log/app.log diff --git a/README.md b/README.md index e9b8235c..5775ad51 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,33 @@ -# Simpler +Создайте на гитхабе форк учебного проекта из скринкаста (ссылки в дополнительных материалах), в полученном новом репозитории, в новой ветке выполните следующие задания: -**Simpler** is a little web framework written in [Ruby](https://www.ruby-lang.org) language. It's compatible with [Rack](https://rack.github.io) interface and intended to **learn** how web frameworks work in general. +Реализуйте расширенные возможности метода render которые позволят возвращать ответ в других форматах, например: +render plain: "Plain text response" + +Реализуйте возможность устанавливать в методе контроллера статус ответа, например: +status 201 + +Реализуйте возможность устанавливать в методе контроллера заголовки, например: +headers['Content-Type'] = 'text/plain' + +Реализуйте механизм обработки исключения когда маршрут для запрашиваемого URL не был найден. В этом случае клиенту должен отдаваться ответ со статусом 404 -## The application overview -Simpler application is a singleton instance of the `Simpler::Application` class. For convenience it can be obtained by calling `Simpler.application` method. This instance holds all the routes and responds to `call` method which is required by the Rack interface. +Напишите механизм разбора route-параметров. Например, при добавлении маршрута +get '/tests/:id', 'tests#show' + + а) Маршрут должен корректно обрабатывать GET запрос +/tests/101 + + b) В методе show контроллера при вызове метода params должен быть доступен параметр :id со значением 101 + +Напишите middleware для логирования HTTP-запросов и ответов: + a) Лог должен записываться в файл log/app.log + b) Для запросов необходимо записывать HTTP-метод запроса, URL, контроллер и метод который будет обрабатывать запрос, хэш параметров который будет доступен при вызове метода params + c) Для ответов необходимо записывать код статуса ответа, тип тела ответа и название шаблона (если ответ рендерился с помощью шаблона представления) + +Пример: + +Request: GET /tests?category=Backend +Handler: TestsController#index +Parameters: {'category' => 'Backend'} +Response: 200 OK [text/html] tests/index.html.erb diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb index 1526a689..30031d17 100644 --- a/app/controllers/tests_controller.rb +++ b/app/controllers/tests_controller.rb @@ -2,10 +2,15 @@ class TestsController < Simpler::Controller def index @time = Time.now + @tests = Test.all end def create + render plain: "Test create!" + end + def show + @id = params[:id] end end diff --git a/app/views/tests/index.html.erb b/app/views/tests/index.html.erb index 39fce580..7ef02c33 100644 --- a/app/views/tests/index.html.erb +++ b/app/views/tests/index.html.erb @@ -8,5 +8,10 @@

Simpler framework at work!

<%= @time %>

+ - \ No newline at end of file + diff --git a/app/views/tests/list.html.erb b/app/views/tests/list.html.erb index 0d430491..33894ed7 100644 --- a/app/views/tests/list.html.erb +++ b/app/views/tests/list.html.erb @@ -9,4 +9,4 @@

<%= @time %>

- \ No newline at end of file + diff --git a/app/views/tests/show.html.erb b/app/views/tests/show.html.erb new file mode 100644 index 00000000..f09cf727 --- /dev/null +++ b/app/views/tests/show.html.erb @@ -0,0 +1,11 @@ + + + + + Show | Simpler application + + +

Simpler framework at work!

+

<%= @id %>

+ + diff --git a/config.ru b/config.ru index 3060cc20..213519bb 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,5 @@ require_relative 'config/environment' +require_relative 'middleware/logger' +use AppLogger, logdev: File.expand_path('log/app.log', __dir__) run Simpler.application diff --git a/config/environment.rb b/config/environment.rb index 7a0d38c3..e4c2c71f 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,3 +1,4 @@ +require 'rack' require_relative '../lib/simpler' Simpler.application.bootstrap! diff --git a/config/routes.rb b/config/routes.rb index 4a751251..da59b495 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Simpler.application.routes do get '/tests', 'tests#index' + get '/tests/:id', 'tests#show' post '/tests', 'tests#create' end diff --git a/db/test_guru.sqlite b/db/test_guru.sqlite index 8624a03a..e09e2f3b 100644 Binary files a/db/test_guru.sqlite and b/db/test_guru.sqlite differ diff --git a/lib/simpler/application.rb b/lib/simpler/application.rb index 711946a9..58a0b7ae 100644 --- a/lib/simpler/application.rb +++ b/lib/simpler/application.rb @@ -28,14 +28,26 @@ def routes(&block) def call(env) route = @router.route_for(env) - controller = route.controller.new(env) - action = route.action + if route + controller = route.controller.new(env) + action = route.action - make_response(controller, action) + make_response(controller, action) + else + status404 + end end private + def status404 + [ + 404, + { 'content-type' => 'text/plain' }, + ['404 page is not faund'] + ] + end + def require_app Dir["#{Simpler.root}/app/**/*.rb"].each { |file| require file } end diff --git a/lib/simpler/controller.rb b/lib/simpler/controller.rb index 9383b035..f2714374 100644 --- a/lib/simpler/controller.rb +++ b/lib/simpler/controller.rb @@ -14,6 +14,7 @@ def initialize(env) def make_response(action) @request.env['simpler.controller'] = self @request.env['simpler.action'] = action + @request.env['simpler.handler'] = "#{self.class.name}##{action}" set_default_headers send(action) @@ -22,14 +23,34 @@ def make_response(action) @response.finish end + def params + @params ||= route_info.merge(request_params) + end + + def request_params + @request.params + end + + def route_info + @request.env['simpler.route_info'] + end + private + def headers(content_type, type) + @response[content_type] = type + end + + def status(code) + @response.status = code + end + def extract_name self.class.name.match('(?.+)Controller')[:name].downcase end def set_default_headers - @response['Content-Type'] = 'text/html' + @response['Content-Type'] ||= 'text/html' end def write_response @@ -42,11 +63,14 @@ def render_body View.new(@request.env).render(binding) end - def params - @request.params + def redirect_to(uri) + [302, { "Location" => uri }, []] end def render(template) + if Hash(template)[:plain] + @response['Content-Type'] = 'text/plain' + end @request.env['simpler.template'] = template end diff --git a/lib/simpler/router.rb b/lib/simpler/router.rb index 14b3415c..691ff6e6 100644 --- a/lib/simpler/router.rb +++ b/lib/simpler/router.rb @@ -19,20 +19,35 @@ def route_for(env) method = env['REQUEST_METHOD'].downcase.to_sym path = env['PATH_INFO'] - @routes.find { |route| route.match?(method, path) } - end + route = @routes.find { |route| route.match?(method, path) } + env['simpler.route_info'] = route.route_info(env) if route + route + end private def add_route(method, path, route_point) + route_point = route_point.split('#') controller = controller_from_string(route_point[0]) action = route_point[1] + route = Route.new(method, path, controller, action) @routes.push(route) + end + def controller_name + "#{route_info[:resource].capitalize}Controller" + end + + def controller_class + Object.const_get(controller_name) + rescue NameError + nil + end + def controller_from_string(controller_name) Object.const_get("#{controller_name.capitalize}Controller") end diff --git a/lib/simpler/router/route.rb b/lib/simpler/router/route.rb index 4c66b4b7..121be64e 100644 --- a/lib/simpler/router/route.rb +++ b/lib/simpler/router/route.rb @@ -9,12 +9,53 @@ def initialize(method, path, controller, action) @path = path @controller = controller @action = action + @path_regexp = make_path_regexp end def match?(method, path) - @method == method && path.match(@path) + @method == method && match_path(path) end + def route_info(env) + path = env['PATH_INFO'] + @route_info ||= begin + resource = path_fragments(path)[0] || "base" + id, action = find_id_and_action(path_fragments(path)[1]) + { resource: resource, action: action, id: id } + end + end + + def find_id_and_action(fragment) + case fragment + when "new" + [nil, :new] + else + [fragment, :show] + end + end + + def match_path(path) + path.match(@path_regexp) + end + + def make_path_regexp + path_parts = @path.split('/') + path_parts.map! do |part| + if part[0] == ":" + part.delete!(':') + part = "(?<#{part}>\\w+)" + else + part + end + end + str_regexp = path_parts.join("\\/") + /#{str_regexp}$/ + end + + def path_fragments(path) + @fragments ||= path.split("/").reject { |s| s.empty? } + end + end end end diff --git a/lib/simpler/view.rb b/lib/simpler/view.rb index 19a73b34..ba0fd8ad 100644 --- a/lib/simpler/view.rb +++ b/lib/simpler/view.rb @@ -10,9 +10,14 @@ def initialize(env) end def render(binding) - template = File.read(template_path) - - ERB.new(template).result(binding) + plain = Hash(template)[:plain] + if plain + "#{plain}\n" + else + template = File.read(template_path) + + ERB.new(template).result(binding) + end end private diff --git a/middleware/logger.rb b/middleware/logger.rb new file mode 100644 index 00000000..8fe2a755 --- /dev/null +++ b/middleware/logger.rb @@ -0,0 +1,31 @@ +require 'logger' + +class AppLogger + + def initialize(app, **options) + @logger = Logger.new(options[:logdev] || STROUT) + @app = app + end + + def call(env) + request_line = "\nRequest: #{env["REQUEST_METHOD"]} " + request_line << env["PATH_INFO"] + request_line << "/?#{env["QUERY_STRING"]}" unless env["QUERY_STRING"].empty? + @logger.info request_line + + status, headers, body = @app.call(env) + + if env['simpler.controller'] + @logger.info "\nParameters: #{env['simpler.controller'].params}" + end + @logger.info "\nHandler: #{env['simpler.handler']}" + + response_line = "\nResponse: #{status} " + response_line << "[#{headers['Content-Type']}]" + response_line << " #{env['simpler.template']}" + @logger.info response_line + + [status, headers, body] + end + +end