require_relative "atd/version" require "rack" require_relative "atd/builtin_class_modifications" require_relative "atd/middleware" require_relative "atd/routes" # The assistant technical director of your website. It does the dirty work so you can see the big picture. module ATD # Creates a new ATD App based on the template of {ATD::App}. # @return [ATD::App] # @param [Symbol] name The name of the new app and new class generated. def new(name) app = Class.new(App) Object.const_set(name.to_sym, app) app end # So called because each instance stores a route, and will be called if that route is reached. # A route for the purposes of {ATD} is a parser that ***will be fed env in {ATD::App#call the rack app}***. class Route attr_accessor :args, :method, :block, :path, :output, :app # The first two arguments must me the path and the output. ***Until this code is updated it is impossible to specify output in a block*** def initialize(*args, &block) args -= [nil] args.reject!(&:empty?) @block = block @path = args.shift @output = args.shift @args = args @app = :DefaultApp end # This works differently from a standard setter because is makes sure that a {Route} can belong to only one {App}. def app=(app_name) old_app = Object.const_get(@app) new_app = Object.const_get(app_name.to_sym) old_app.routes -= self if old_app.routes.is_a?(Array) && old_app.routes.include?(self) new_app.routes.nil? ? new_app.routes = Array(self) : new_app.routes += Array(self) @app = app_name end # @!method get(path = nil,*args) # @param [String] path The path at which the route should receive from. # @return ATD::Route # Sets route to receive a get request to path and execute the block provided (if one is provided) # @!method post(path = nil,*args) # @param [String] path The path at which the route should receive from. # @return ATD::Route # Sets route to receive a post request to path and execute the block provided (if one is provided) # @!method put(path = nil,*args) # @param [String] path The path at which the route should receive from. # @return ATD::Route # Sets route to receive a put request to path and execute the block provided (if one is provided) # @!method patch(path = nil,*args) # @param [String] path The path at which the route should receive from. # @return ATD::Route # Sets route to receive a patch request to path and execute the block provided (if one is provided) # @!method delete(path = nil,*args) # @param [String] path The path at which the route should receive from. # @return ATD::Route # Sets route to receive a delete request to path and execute the block provided (if one is provided) [:get, :post, :put, :delete, :patch].each do |method| define_method(method) do |path = nil, *args, &block| @block = block @method = method @output = args.shift if @output.nil? @path = path if @path.nil? @args = @args.concat(args) - [nil] unless args.nil? self end end # Converts an instance of {ATD::Route} into it's Hash representation. # The format for the Hash is listed {ATD::App#initialize here} # @api private def to_h routes = {} routes[@path] = {} routes[@path][@method] = {} routes[@path][@method] = { output: @output, block: @block, args: @args, route: self } routes end end # A template {App} that all Apps extend. When a new App is created with {ATD.new ATD.new} it extends this class. class App class << self attr_accessor :routes # An array of instances of {ATD::Route} that belong to this {App}. attr_accessor :http # Generates an instance of {ATD::Route}. # Passes all arguments and the block to {Route.new the constructor} and sets the app where it was called from. def request(*args, &block) route = ATD::Route.new(*args, &block) route.app = (self == Object || self == ATD::App ? :DefaultApp : name.to_sym) route end alias req request alias r request end # Sets up the @routes instance variable from the {.routes} class instance variable. # Can be passed an array of instances of {ATD::Route} and they will be added to @routes. # The format of the new @routes instance variable is: # {"/" => { # get: {output: "Hello World", # block: Proc.new}, # post: {output: "Hello World", # block: Proc.new} # }, # "/hello" => { # get: {output: "Hello World", # block: Proc.new}, # post: {output: "Hello World", # block: Proc.new # } # } # } # @param [Array] routes An array of instances of {ATD::Route}. def initialize(routes = []) Middleware.setup(ATD::Route) routes.each do |route| Middleware.run(route) end routes += self.class.routes @routes = {} routes.each do |route| Middleware.run(route) @routes = @routes.to_h.deep_merge(route.to_h) end end # Starts the rack server # @param [Class] server The server that you would like to use. # @param [Fixnum] port The port you would like the server to run on. def start(server = WEBrick, port = 3150) Rack::Server.start(app: self, server: server, Port: port) end # This is the method which responds to .call, as the Rack spec requires. # It will return status code 200 and whatever output corresponds the that route if it exists, and if it doesn't # it will return status code 404 and the message "Error 404" def call(env) route = route(env) return error(404) if route.nil? return [route[:status_code].to_i, Hash(route[:headers]), Array(route[:output])] if route[:block].nil? http output: route[:output], request: Rack::Request.new(env), method: env["REQUEST_METHOD"] run_block(route[:block]) end private def route(env) return nil if @routes[env["PATH_INFO"]].nil? return @routes[env["PATH_INFO"]][nil] unless @routes[env["PATH_INFO"]][nil].nil? @routes[env["PATH_INFO"]][env["REQUEST_METHOD"].downcase.to_sym] end def run_block(block) block.call Middleware.run(self) [self.class.http[:status_code].to_i, Hash(self.class.http[:headers]), Array(self.class.http[:output])] end def http(additional_params) self.class.http = { status_code: 200, headers: {} }.merge(additional_params) end def error(number) [number, {}, ["Error #{number}"]] end end module_function :new end # @return [ATD::Route] def request(*args, &block) ATD::App.request(args, block) end alias req request alias r request ATD.new("DefaultApp")