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 = '%