From d3fc6e2f32aa88305fd817870fb9cceabf4713be Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Fri, 30 May 2025 18:50:39 +0300 Subject: [PATCH 1/9] fix --- Gemfile | 11 +++++++++++ Gemfile.lock | 29 +++++++++++++++++++++++++++++ lib/simpler/application.rb | 1 + 3 files changed, 41 insertions(+) create mode 100644 Gemfile create mode 100644 Gemfile.lock diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..e47072ae --- /dev/null +++ b/Gemfile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Gemfile +ruby '3.3.3' +source 'https://rubygems.org' + +gem 'puma' +gem 'rackup' +gem 'sequel' +gem 'sqlite3' +gem 'rack' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..2201a43d --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,29 @@ +GEM + remote: https://rubygems.org/ + specs: + bigdecimal (3.2.0) + nio4r (2.7.4) + puma (6.6.0) + nio4r (~> 2.0) + rack (3.1.15) + rackup (2.2.1) + rack (>= 3) + sequel (5.92.0) + bigdecimal + sqlite3 (1.7.3-arm64-darwin) + +PLATFORMS + arm64-darwin-24 + +DEPENDENCIES + puma + rack + rackup + sequel + sqlite3 + +RUBY VERSION + ruby 3.3.3p89 + +BUNDLED WITH + 2.6.9 diff --git a/lib/simpler/application.rb b/lib/simpler/application.rb index 711946a9..b3cc6aa2 100644 --- a/lib/simpler/application.rb +++ b/lib/simpler/application.rb @@ -1,3 +1,4 @@ +require 'rack' require 'yaml' require 'singleton' require 'sequel' From 4d0d672a32487e4f6934693a69bef89b4e877fbc Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 3 Jun 2025 13:26:13 +0300 Subject: [PATCH 2/9] added plain and json response --- app/controllers/tests_controller.rb | 5 ++- config/routes.rb | 1 + lib/simpler/controller.rb | 52 +++++++++++++++++++++++++---- lib/simpler/router/route.rb | 4 +-- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb index 1526a689..c789299c 100644 --- a/app/controllers/tests_controller.rb +++ b/app/controllers/tests_controller.rb @@ -5,7 +5,10 @@ def index end def create - + render plain: "Plain text response" end + def json + render json: { status: 'ok', time: Time.now } + end end diff --git a/config/routes.rb b/config/routes.rb index 4a751251..39fa67e6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Simpler.application.routes do get '/tests', 'tests#index' + get '/tests/json', 'tests#json' post '/tests', 'tests#create' end diff --git a/lib/simpler/controller.rb b/lib/simpler/controller.rb index 9383b035..05b1d5d3 100644 --- a/lib/simpler/controller.rb +++ b/lib/simpler/controller.rb @@ -1,3 +1,4 @@ +require 'json' require_relative 'view' module Simpler @@ -9,6 +10,7 @@ def initialize(env) @name = extract_name @request = Rack::Request.new(env) @response = Rack::Response.new + @env = env end def make_response(action) @@ -29,25 +31,61 @@ def extract_name end def set_default_headers - @response['Content-Type'] = 'text/html' + @response['Content-Type'] ||= 'text/html' end def write_response - body = render_body - - @response.write(body) + @response.write(render_body) end def render_body - View.new(@request.env).render(binding) + return render_json if json_response? + return render_plain if plain_response? + + render_template + end + + def plain_response? + @env.key?('simpler.plain') + end + + def json_response? + @env.key?('simpler.json') + end + + def render_plain + @response['Content-Type'] = 'text/plain' + @env['simpler.plain'] + end + + def render_json + @response['Content-Type'] = 'application/json' + JSON.generate(@env['simpler.json']) + end + + def render_template + View.new(@env).render(binding) end def params @request.params end - def render(template) - @request.env['simpler.template'] = template + def render(options) + case options + when String, Symbol + @env['simpler.template'] = options.to_s + when Hash + if options[:plain] + @env['simpler.plain'] = options[:plain].to_s + elsif options[:json] + @env['simpler.json'] = options[:json] + else + raise ArgumentError, "Unknown render option: #{options.keys.join(', ')}" + end + else + raise ArgumentError, "Invalid argument for render: #{options.inspect}" + end end end diff --git a/lib/simpler/router/route.rb b/lib/simpler/router/route.rb index 4c66b4b7..32f496b7 100644 --- a/lib/simpler/router/route.rb +++ b/lib/simpler/router/route.rb @@ -11,8 +11,8 @@ def initialize(method, path, controller, action) @action = action end - def match?(method, path) - @method == method && path.match(@path) + def match?(method, request_path) + @method == method && request_path == @path end end From ecd13d9014ac8fcec693d1683446fb3bd3410cf4 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 3 Jun 2025 14:31:10 +0300 Subject: [PATCH 3/9] fixed an error detecting the controller when the path ends with a slash. --- lib/simpler/router/route.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/simpler/router/route.rb b/lib/simpler/router/route.rb index 32f496b7..adc5f17a 100644 --- a/lib/simpler/router/route.rb +++ b/lib/simpler/router/route.rb @@ -1,20 +1,24 @@ module Simpler class Router class Route - attr_reader :controller, :action def initialize(method, path, controller, action) @method = method - @path = path + @path = normalize_path(path) @controller = controller @action = action end def match?(method, request_path) - @method == method && request_path == @path + @method == method && normalize_path(request_path) == @path end + private + + def normalize_path(path) + path.chomp('/') + end end end -end +end \ No newline at end of file From 75e76efd86f1f1c1523d46b7f03a5bffddb99e8b Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 3 Jun 2025 14:35:10 +0300 Subject: [PATCH 4/9] response status added --- app/controllers/tests_controller.rb | 8 ++++---- config/routes.rb | 2 +- lib/simpler/controller.rb | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb index c789299c..994e9772 100644 --- a/app/controllers/tests_controller.rb +++ b/app/controllers/tests_controller.rb @@ -5,10 +5,10 @@ def index end def create - render plain: "Plain text response" + render json: { message: 'Created successfully' }, status: 201 end - def json - render json: { status: 'ok', time: Time.now } + def plain + render plain: "Time.now: #{Time.now}\n" end -end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 39fa67e6..bdf719b3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,5 @@ Simpler.application.routes do get '/tests', 'tests#index' - get '/tests/json', 'tests#json' + get '/tests/plain', 'tests#plain' post '/tests', 'tests#create' end diff --git a/lib/simpler/controller.rb b/lib/simpler/controller.rb index 05b1d5d3..ab5d652b 100644 --- a/lib/simpler/controller.rb +++ b/lib/simpler/controller.rb @@ -76,6 +76,7 @@ def render(options) when String, Symbol @env['simpler.template'] = options.to_s when Hash + @response.status = options.delete(:status) if options.key?(:status) if options[:plain] @env['simpler.plain'] = options[:plain].to_s elsif options[:json] From 82fbbdba467a3a8dd6a1a43575fd9cc31e93b0c4 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 3 Jun 2025 15:03:21 +0300 Subject: [PATCH 5/9] added headers --- app/controllers/tests_controller.rb | 1 + lib/simpler/controller.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb index 994e9772..5f4281a8 100644 --- a/app/controllers/tests_controller.rb +++ b/app/controllers/tests_controller.rb @@ -9,6 +9,7 @@ def create end def plain + headers['X-Time-Header'] = Time.now.to_s render plain: "Time.now: #{Time.now}\n" end end \ No newline at end of file diff --git a/lib/simpler/controller.rb b/lib/simpler/controller.rb index ab5d652b..556bf164 100644 --- a/lib/simpler/controller.rb +++ b/lib/simpler/controller.rb @@ -24,6 +24,10 @@ def make_response(action) @response.finish end + def headers + @response.headers + end + private def extract_name From cce9c621593a6a62d427350860272cac38d6e29a Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 3 Jun 2025 15:40:57 +0300 Subject: [PATCH 6/9] added not_found_response --- lib/simpler/application.rb | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/simpler/application.rb b/lib/simpler/application.rb index b3cc6aa2..f763233a 100644 --- a/lib/simpler/application.rb +++ b/lib/simpler/application.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rack' require 'yaml' require 'singleton' @@ -7,7 +9,6 @@ module Simpler class Application - include Singleton attr_reader :db @@ -29,10 +30,13 @@ def routes(&block) def call(env) route = @router.route_for(env) - controller = route.controller.new(env) - action = route.action - - make_response(controller, action) + if route.nil? + not_found_response + else + controller = route.controller.new(env) + action = route.action + make_response(controller, action) + end end private @@ -51,9 +55,20 @@ def setup_database @db = Sequel.connect(database_config) end + def not_found_response + build_error_response(404, 'Not Found') + end + + def build_error_response(status, body) + Rack::Response.new do |res| + res.status = status + res['Content-Type'] = 'text/plain' + res.write(body) + end.finish + end + def make_response(controller, action) controller.make_response(action) end - end end From c6b74712d5f9c312cbacaa5bdfbfcdd4b17ec096 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 3 Jun 2025 15:41:28 +0300 Subject: [PATCH 7/9] rubocop --- Gemfile | 3 ++- Gemfile.lock | 23 +++++++++++++++++++++++ app/controllers/tests_controller.rb | 5 +++-- app/models/test.rb | 3 ++- config.ru | 2 ++ config/environment.rb | 2 ++ config/routes.rb | 2 ++ lib/simpler.rb | 4 ++-- lib/simpler/controller.rb | 4 ++-- lib/simpler/router.rb | 4 ++-- lib/simpler/router/route.rb | 4 +++- lib/simpler/view.rb | 6 +++--- 12 files changed, 48 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index e47072ae..019be792 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,8 @@ ruby '3.3.3' source 'https://rubygems.org' gem 'puma' +gem 'rack' gem 'rackup' +gem 'rubocop', require: false gem 'sequel' gem 'sqlite3' -gem 'rack' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 2201a43d..8d0d87c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,16 +1,38 @@ GEM remote: https://rubygems.org/ specs: + ast (2.4.2) bigdecimal (3.2.0) + json (2.11.3) nio4r (2.7.4) + parallel (1.23.0) + parser (3.2.2.1) + ast (~> 2.4.1) puma (6.6.0) nio4r (~> 2.0) rack (3.1.15) rackup (2.2.1) rack (>= 3) + rainbow (3.1.1) + regexp_parser (2.10.0) + rexml (3.4.1) + rubocop (1.50.2) + json (~> 2.3) + parallel (~> 1.10) + parser (>= 3.2.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.28.1) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) sequel (5.92.0) bigdecimal sqlite3 (1.7.3-arm64-darwin) + unicode-display_width (2.4.2) PLATFORMS arm64-darwin-24 @@ -19,6 +41,7 @@ DEPENDENCIES puma rack rackup + rubocop sequel sqlite3 diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb index 5f4281a8..be9a04d9 100644 --- a/app/controllers/tests_controller.rb +++ b/app/controllers/tests_controller.rb @@ -1,5 +1,6 @@ -class TestsController < Simpler::Controller +# frozen_string_literal: true +class TestsController < Simpler::Controller def index @time = Time.now end @@ -12,4 +13,4 @@ def plain headers['X-Time-Header'] = Time.now.to_s render plain: "Time.now: #{Time.now}\n" end -end \ No newline at end of file +end diff --git a/app/models/test.rb b/app/models/test.rb index 86376668..db135e04 100644 --- a/app/models/test.rb +++ b/app/models/test.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # Simpler.application.db.create_table(:tests) do # primary_key :id # String :title, null: false # Integer :level, default: 0 # end class Test < Sequel::Model - end diff --git a/config.ru b/config.ru index 3060cc20..91b5a1bf 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative 'config/environment' run Simpler.application diff --git a/config/environment.rb b/config/environment.rb index 7a0d38c3..a566c8d3 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative '../lib/simpler' Simpler.application.bootstrap! diff --git a/config/routes.rb b/config/routes.rb index bdf719b3..ba278ab4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Simpler.application.routes do get '/tests', 'tests#index' get '/tests/plain', 'tests#plain' diff --git a/lib/simpler.rb b/lib/simpler.rb index f9dfe3c4..6fb23105 100644 --- a/lib/simpler.rb +++ b/lib/simpler.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require 'pathname' require_relative 'simpler/application' module Simpler - class << self def application Application.instance @@ -12,5 +13,4 @@ def root Pathname.new(File.expand_path('..', __dir__)) end end - end diff --git a/lib/simpler/controller.rb b/lib/simpler/controller.rb index 556bf164..3590a502 100644 --- a/lib/simpler/controller.rb +++ b/lib/simpler/controller.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + require 'json' require_relative 'view' module Simpler class Controller - attr_reader :name, :request, :response def initialize(env) @@ -92,6 +93,5 @@ def render(options) raise ArgumentError, "Invalid argument for render: #{options.inspect}" end end - end end diff --git a/lib/simpler/router.rb b/lib/simpler/router.rb index 14b3415c..07469123 100644 --- a/lib/simpler/router.rb +++ b/lib/simpler/router.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require_relative 'router/route' module Simpler class Router - def initialize @routes = [] end @@ -36,6 +37,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 adc5f17a..886bfeec 100644 --- a/lib/simpler/router/route.rb +++ b/lib/simpler/router/route.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Simpler class Router class Route @@ -21,4 +23,4 @@ def normalize_path(path) end end end -end \ No newline at end of file +end diff --git a/lib/simpler/view.rb b/lib/simpler/view.rb index 19a73b34..5c6e7a25 100644 --- a/lib/simpler/view.rb +++ b/lib/simpler/view.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + require 'erb' module Simpler class View - - VIEW_BASE_PATH = 'app/views'.freeze + VIEW_BASE_PATH = 'app/views' def initialize(env) @env = env @@ -34,6 +35,5 @@ def template_path Simpler.root.join(VIEW_BASE_PATH, "#{path}.html.erb") end - end end From c9cde1fa3a8d3d0d697542536319b7ff3112f62b Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 3 Jun 2025 16:10:57 +0300 Subject: [PATCH 8/9] added parsing params --- app/controllers/tests_controller.rb | 5 +++++ config/routes.rb | 1 + lib/simpler/controller.rb | 7 ++++++- lib/simpler/router.rb | 11 ++++++---- lib/simpler/router/route.rb | 31 +++++++++++++++++++++++++++-- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb index be9a04d9..222884a8 100644 --- a/app/controllers/tests_controller.rb +++ b/app/controllers/tests_controller.rb @@ -13,4 +13,9 @@ def plain headers['X-Time-Header'] = Time.now.to_s render plain: "Time.now: #{Time.now}\n" end + + def show + @test_id = params[:id] + render plain: "Test ID: #{@test_id}" + end end diff --git a/config/routes.rb b/config/routes.rb index ba278ab4..55ad3d6f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,4 +4,5 @@ get '/tests', 'tests#index' get '/tests/plain', 'tests#plain' post '/tests', 'tests#create' + get '/tests/:id', 'tests#show' end diff --git a/lib/simpler/controller.rb b/lib/simpler/controller.rb index 3590a502..7a514d70 100644 --- a/lib/simpler/controller.rb +++ b/lib/simpler/controller.rb @@ -73,7 +73,12 @@ def render_template end def params - @request.params + return @params if @params + + @params = @request.params.transform_keys(&:to_sym) + route_params = @env['simpler.route_params']&.transform_keys(&:to_sym) + @params.merge!(route_params) if route_params + @params end def render(options) diff --git a/lib/simpler/router.rb b/lib/simpler/router.rb index 07469123..816ed98a 100644 --- a/lib/simpler/router.rb +++ b/lib/simpler/router.rb @@ -20,15 +20,18 @@ def route_for(env) method = env['REQUEST_METHOD'].downcase.to_sym path = env['PATH_INFO'] - @routes.find { |route| route.match?(method, path) } + route = @routes.find { |r| r.match?(method, path) } + return unless route + + env['simpler.route_params'] = route.extract_params(path) + 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] + controller_name, action = route_point.split('#') + controller = controller_from_string(controller_name) route = Route.new(method, path, controller, action) @routes.push(route) diff --git a/lib/simpler/router/route.rb b/lib/simpler/router/route.rb index 886bfeec..12a4abcc 100644 --- a/lib/simpler/router/route.rb +++ b/lib/simpler/router/route.rb @@ -3,17 +3,27 @@ module Simpler class Router class Route - attr_reader :controller, :action + attr_reader :controller, :action, :params_pattern def initialize(method, path, controller, action) @method = method @path = normalize_path(path) @controller = controller @action = action + @params_pattern = build_params_pattern(path) end def match?(method, request_path) - @method == method && normalize_path(request_path) == @path + @method == method && path_match?(normalize_path(request_path)) + end + + def extract_params(request_path) + return {} unless @params_pattern + + matches = normalize_path(request_path).match(@params_pattern) + return {} unless matches + + matches.named_captures.transform_keys(&:to_sym) end private @@ -21,6 +31,23 @@ def match?(method, request_path) def normalize_path(path) path.chomp('/') end + + def build_params_pattern(path) + return nil unless path.include?(':') + + pattern = normalize_path(path).gsub(%r{:[^/]+}) do |match| + param_name = match[1..] + "(?<#{param_name}>[^\/]+)" + end + + Regexp.new("^#{pattern}$") + end + + def path_match?(request_path) + return request_path == @path unless @params_pattern + + request_path.match?(@params_pattern) + end end end end From 2d09f3add240b4dd301bb8ecd400b43a6cafb7d2 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 3 Jun 2025 16:43:44 +0300 Subject: [PATCH 9/9] added log middleware --- config.ru | 3 +- config/environment.rb | 1 + lib/simpler/middleware/logger.rb | 74 ++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 lib/simpler/middleware/logger.rb diff --git a/config.ru b/config.ru index 91b5a1bf..c92e3187 100644 --- a/config.ru +++ b/config.ru @@ -1,5 +1,6 @@ # frozen_string_literal: true -require_relative 'config/environment' +require_relative './config/environment' +use Simpler::Middleware::Logger run Simpler.application diff --git a/config/environment.rb b/config/environment.rb index a566c8d3..c037546f 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative '../lib/simpler/middleware/logger' require_relative '../lib/simpler' Simpler.application.bootstrap! diff --git a/lib/simpler/middleware/logger.rb b/lib/simpler/middleware/logger.rb new file mode 100644 index 00000000..48f57c5e --- /dev/null +++ b/lib/simpler/middleware/logger.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'logger' +require 'fileutils' + +module Simpler + module Middleware + class Logger + LOG_FILE = 'log/app.log' + + def initialize(app) + @app = app + ensure_log_directory + @logger = setup_logger + end + + def call(env) + start = Time.now + status, headers, body = @app.call(env) + + log_request(env) + log_response(status, headers, env) + + [status, headers, body] + end + + private + + def ensure_log_directory + FileUtils.mkdir_p(File.dirname(LOG_FILE)) + end + + def setup_logger + ::Logger.new(LOG_FILE, 'daily') + end + + def log_request(env) + request = Rack::Request.new(env) + controller = env['simpler.controller'] + action = env['simpler.action'] + + message = [] + message << "Request: #{env['REQUEST_METHOD']} #{env['PATH_INFO']}#{env['QUERY_STRING'].empty? ? '' : "?#{env['QUERY_STRING']}"}" + message << "Handler: #{controller.class.name}##{action}" if controller && action + message << "Parameters: #{collect_parameters(request, env)}" + + @logger.info(message.join("\n")) + end + + def log_response(status, headers, env) + message = "Response: #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" + message << " [#{headers['Content-Type']}]" + message << " #{env['simpler.template']}" if env['simpler.template'] + + @logger.info(message) + end + + def collect_parameters(request, env) + params = {} + + # Query parameters + params.merge!(request.GET) + + # POST parameters + params.merge!(request.POST) if request.post? + + # Route parameters + params.merge!(env['simpler.route_params'] || {}) + + params + end + end + end +end