diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb index 1526a689..b0868081 100644 --- a/app/controllers/tests_controller.rb +++ b/app/controllers/tests_controller.rb @@ -1,11 +1,32 @@ class TestsController < Simpler::Controller - def index @time = Time.now end - def create + def plain + render plain: 'Это простой текстовый ответ' + end + def json + data = { message: 'Это JSON', time: Time.now } + render json: data end + def html + render html: '

Это HTML

' + end + + def xml + data = { message: 'Это XML', time: Time.now } + render xml: data + end + + def show + render plain: "Запрошен тест с ID: #{params[:id]}" + end + + def custom_status + status 201 + render plain: 'Создано успешно!' + 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/application.rb b/config/application.rb new file mode 100644 index 00000000..9ae67307 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,8 @@ +require_relative '../lib/simpler/middleware/logger' + +class Application < Simpler::Application + def initialize + super + use Simpler::Middleware::Logger + end +end diff --git a/config/routes.rb b/config/routes.rb index 4a751251..406062b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,9 @@ Simpler.application.routes do get '/tests', 'tests#index' - post '/tests', 'tests#create' + get '/tests/plain', 'tests#plain' + get '/tests/json', 'tests#json' + get '/tests/html', 'tests#html' + get '/tests/xml', 'tests#xml' + get '/tests/:id', 'tests#show' + post '/tests/status', 'tests#custom_status' 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..b7e0df56 100644 --- a/lib/simpler/application.rb +++ b/lib/simpler/application.rb @@ -6,13 +6,15 @@ module Simpler class Application - include Singleton - attr_reader :db + DEFAULT_SEQUEL_EXTENSIONS = %i[pagination query_literals].freeze + + attr_reader :db, :router, :middleware def initialize @router = Router.new + @middleware = [] @db = nil end @@ -23,21 +25,39 @@ def bootstrap! end def routes(&block) - @router.instance_eval(&block) + router.instance_eval(&block) end def call(env) - route = @router.route_for(env) + route = router.route_for(env) + env['simpler.route'] = route controller = route.controller.new(env) action = route.action make_response(controller, action) + rescue Router::RouteNotFoundError => e + make_not_found_response(e) + rescue StandardError => e + make_error_response(e) + end + + def use(middleware_class, *args) + middleware << proc { |app| middleware_class.new(app, *args) } + end + + def to_app + return @app if @app + + builder = Rack::Builder.new + apply_middleware(builder) + builder.run(self) + @app = builder.to_app end private def require_app - Dir["#{Simpler.root}/app/**/*.rb"].each { |file| require file } + Dir[File.join(Simpler.root, 'app', '**', '*.rb')].sort.each { |file| require file } end def require_routes @@ -45,14 +65,53 @@ def require_routes end def setup_database - database_config = YAML.load_file(Simpler.root.join('config/database.yml')) - database_config['database'] = Simpler.root.join(database_config['database']) + database_config = load_database_config @db = Sequel.connect(database_config) + setup_database_extensions + end + + def load_database_config + config = YAML.safe_load( + File.read(Simpler.root.join('config/database.yml')), + permitted_classes: [Symbol] + ) + config['database'] = Simpler.root.join(config['database']) + config + end + + def setup_database_extensions + DEFAULT_SEQUEL_EXTENSIONS.each { |ext| @db.extension(ext) } + end + + def apply_middleware(builder) + middleware.each { |m| builder.use(m) } end def make_response(controller, action) controller.make_response(action) end + def make_not_found_response(error) + [ + 404, + { 'Content-Type' => 'text/plain' }, + ["404 Not Found\n\n#{error.message}"] + ] + end + + def make_error_response(error) + log_error(error) + [ + 500, + { 'Content-Type' => 'text/plain' }, + ["500 Internal Server Error\n\n#{error.message}"] + ] + end + + def log_error(error) + logger = Logger.new(Simpler.root.join('log/errors.log')) + logger.error(error.message) + logger.error(error.backtrace.join("\n")) + end end end diff --git a/lib/simpler/controller.rb b/lib/simpler/controller.rb index 9383b035..61fd000c 100644 --- a/lib/simpler/controller.rb +++ b/lib/simpler/controller.rb @@ -2,6 +2,14 @@ module Simpler class Controller + class DoubleRenderError < StandardError; end + + CONTENT_TYPES = { + plain: 'text/plain', + json: 'application/json', + xml: 'application/xml', + html: 'text/html' + }.freeze attr_reader :name, :request, :response @@ -9,46 +17,112 @@ def initialize(env) @name = extract_name @request = Rack::Request.new(env) @response = Rack::Response.new + @render_performed = false end def make_response(action) - @request.env['simpler.controller'] = self - @request.env['simpler.action'] = action - + set_request_context(action) set_default_headers - send(action) - write_response + process_action(action) + ensure_response_written @response.finish end + protected + + def params + @params ||= begin + request_params = @request.params + route_params = @request.env['simpler.route']&.params || {} + request_params.merge(route_params) + end + end + + def render(template_or_options) + ensure_not_rendered + handle_render_options(template_or_options) + write_response + end + + def status(code) + @response.status = code.to_i + end + + def headers + @response.headers + end + private def extract_name - self.class.name.match('(?.+)Controller')[:name].downcase + self.class.name.match('(?.+)Controller')[:n].downcase + end + + def set_request_context(action) + @request.env['simpler.controller'] = self + @request.env['simpler.action'] = action end def set_default_headers - @response['Content-Type'] = 'text/html' + headers['Content-Type'] = CONTENT_TYPES[:html] + end + + def process_action(action) + send(action) + end + + def ensure_response_written + write_response unless @render_performed + end + + def ensure_not_rendered + raise DoubleRenderError, 'Cannot render or redirect more than once per action' if @render_performed + end + + def handle_render_options(template_or_options) + case template_or_options + when String, Symbol + @request.env['simpler.template'] = template_or_options + when Hash + @render_options = template_or_options + end end def write_response - body = render_body + return if @render_performed + body = render_body @response.write(body) + @render_performed = true end def render_body - View.new(@request.env).render(binding) + if @render_options + process_render_options + else + render_template + end end - def params - @request.params + def render_template + View.new(@request.env).render(binding) end - def render(template) - @request.env['simpler.template'] = template + def process_render_options + render_type, content = @render_options.first + return render_template unless CONTENT_TYPES.key?(render_type) + + headers['Content-Type'] = CONTENT_TYPES[render_type] + format_response_body(render_type, content) end + def format_response_body(type, content) + case type + when :json then content.to_json + when :xml then content.to_xml + else content.to_s + end + end end end diff --git a/lib/simpler/middleware/logger.rb b/lib/simpler/middleware/logger.rb new file mode 100644 index 00000000..cd200cd9 --- /dev/null +++ b/lib/simpler/middleware/logger.rb @@ -0,0 +1,109 @@ +require 'logger' +require 'forwardable' + +module Simpler + module Middleware + class Logger + extend Forwardable + + LOG_FORMAT = '%