From cc3de4a3df2eb1b7c379398bbd8ae469f635cbe5 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 24 Aug 2026 11:40:00 +0200 Subject: [PATCH] feat(app): add application with routing dsl, resources, and dispatch --- lib/prism/application.rb | 201 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 lib/prism/application.rb diff --git a/lib/prism/application.rb b/lib/prism/application.rb new file mode 100644 index 0000000..c7eb3ae --- /dev/null +++ b/lib/prism/application.rb @@ -0,0 +1,201 @@ +module Prism + class Application + include Renderable + + class << self + def router + @router ||= Router.new + end + + def middleware_stack + @middleware_stack ||= MiddlewareStack.new + end + + def before_filters + @before_filters ||= [] + end + + def after_filters + @after_filters ||= [] + end + + def prefix_stack + @prefix_stack ||= [] + end + + def settings + @settings ||= { views: 'views' } + end + + def set(key, value) + settings[key] = value + end + + def use(middleware, *args, **kwargs, &block) + middleware_stack.use(middleware, *args, **kwargs, &block) + reset_stack! + end + + def enable_sessions(secret:, **options) + set(:session_secret, secret) + use(Middleware::Session, secret: secret, **options) + end + + def helpers(&block) + class_eval(&block) + end + + %w[get post put patch delete options head].each do |verb| + define_method(verb) do |pattern, to: nil, as: nil, &block| + router.add(verb, join_prefix(pattern), name: as, to: to, &block) + end + end + + def root(to:) + get('/', to: to, as: :root) + end + + def resources(name, only: nil, except: nil, param: :id) + wanted = only || RestActions + wanted = wanted - Array(except) if except + singular = Inflector.singularize(name.to_s) + (RestActions & wanted).each { |action| add_resource_route(name, action, singular, param) } + end + + def group(prefix) + prefix_stack.push(prefix) + yield + ensure + prefix_stack.pop + end + + def before(pattern = nil, &block) + before_filters << [pattern ? PathMatcher.new(join_prefix(pattern)) : nil, block] + end + + def after(pattern = nil, &block) + after_filters << [pattern ? PathMatcher.new(join_prefix(pattern)) : nil, block] + end + + def call(env) + built.call(env) + end + + def built + @built ||= middleware_stack.build(->(env) { new.dispatch(env) }) + end + + def reset_stack! + @built = nil + end + + private + + RestActions = %i[index new create show edit update destroy].freeze + + def add_resource_route(name, action, singular, param) + base = "/#{name}" + controller = name.to_s + case action + when :index then router.add('GET', join_prefix(base), name: name, to: "#{controller}#index") + when :new then router.add('GET', join_prefix("#{base}/new"), name: "new_#{singular}", to: "#{controller}#new") + when :create then router.add('POST', join_prefix(base), to: "#{controller}#create") + when :show then router.add('GET', join_prefix("#{base}/:#{param}"), name: singular, to: "#{controller}#show") + when :edit then router.add('GET', join_prefix("#{base}/:#{param}/edit"), name: "edit_#{singular}", to: "#{controller}#edit") + when :update + router.add('PATCH', join_prefix("#{base}/:#{param}"), to: "#{controller}#update") + router.add('PUT', join_prefix("#{base}/:#{param}"), to: "#{controller}#update") + when :destroy then router.add('DELETE', join_prefix("#{base}/:#{param}"), to: "#{controller}#destroy") + end + end + + def join_prefix(pattern) + return pattern if prefix_stack.empty? + prefix_stack.join + pattern + end + end + + attr_reader :request, :response, :env + + def dispatch(env) + @env = env + @env['prism.router'] = self.class.router + @request = Request.new(env) + @response = Response.new + head = @request.request_method == 'HEAD' + lookup_method = head ? 'GET' : @request.request_method + + catch(:halt) do + run_before_filters + matched = run_routes(lookup_method) + handle_not_found unless matched + end + catch(:halt) do + run_after_filters + end + finalize_flash + + status, headers, body = @response.finish + body = [] if head + [status, headers, body] + end + + def erb(template, locals: {}) + source = template.is_a?(Symbol) ? load_template(template) : template + Template.render(source, locals) + end + alias_method :render, :erb + + private + + def run_before_filters + self.class.before_filters.each do |matcher, block| + next if matcher && matcher.match(@request.path).nil? + instance_exec(&block) + end + end + + def run_after_filters + self.class.after_filters.each do |matcher, block| + next if matcher && matcher.match(@request.path).nil? + instance_exec(&block) + end + end + + def run_routes(lookup_method) + self.class.router.matches(lookup_method, @request.path).each do |route, path_params| + @request.path_params = path_params + outcome = catch(:pass) do + route.controller_route? ? dispatch_to_controller(route) : finalize_body(instance_exec(&route.block)) + :handled + end + return true if outcome == :handled + end + false + end + + def dispatch_to_controller(route) + controller_class = Object.const_get("#{Inflector.camelize(route.controller_name)}Controller") + controller = controller_class.new(@env, route.action_name, @request.path_params) + status, headers, body = controller.process + @response.status = status + headers.each { |name, value| @response[name] = value } + @response.body = body + end + + def finalize_body(result) + return unless @response.body.empty? + @response.body = [result.to_s] if result.is_a?(String) + end + + def handle_not_found + @response.status = 404 + @response.body = ["Not Found: #{@request.request_method} #{@request.path}"] + end + + def load_template(name) + directory = self.class.settings[:views] || 'views' + File.read(File.join(directory, "#{name}.erb")) + end + end +end