require_relative "atd/version" require "rack" require "webrick" require "mime-types" require_relative "atd/internal_helpers" require_relative "atd/builtin_class_modifications" require_relative "atd/internal_helpers" require_relative "atd/routes" # Extension packs # require_relative "extensions/precompilers" # The assistant technical director of your website. It does the dirty work so you can see the big picture. module ATD using Refinements # 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 include InternalHelpers attr_reader :app attr_accessor :args, :method, :block, :path, :output, :actions, :status_code, :filename, :headers # The first two arguments must me the path and the output. def initialize(*args, &block) @args, @block, @path, @output, @actions, @status_code, @filename = nil @status_code = 200 @headers = {} @method = [:get, :post, :put, :patch, :delete] @method = [] if args.last.is_a?(Hash) && !(args.last[:respond_to].nil? || args.last[:ignore].nil?) @app = :DefaultApp parse_args(*args, &block) 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 |*args, &block| # This conditional allows the syntax get post put "/", "Hello" because it passes # the variables up through the different method calls. if args.first.is_a?(ATD::Route) @method = args.first.method @filename = args.first.filename @output = args.first.output @path = args.first.path @args = args.first.args @block = args.first.block @app = args.first.app @actions = args.first.actions end @method = [method] if @method.length == 5 @method += [method] @method.uniq! parse_args(*args, &block) end end private # This should also manage @method at some point def parse_args(path = "", output = "", *args, &block) args = Hash(args[0]) if args.is_a? Array if output.is_a? Hash args.merge(output) output = "" end @args = Hash(@args).merge(args) # rubocop:disable Style/EmptyElse if !path.is_a? ATD::Route @block = block @path = path @output = output else # Maybe here it would make sense to assign path and output into args end # rubocop:enable Style/EmptyElse # @output should be whatever the input is unless the input is a controller/action or the input is_file_string? if @output =~ /^\w*#\w*$/ # Check if @path is a controller#action combo controller, action = @output.split("#") @action = Object.const_get(controller.to_sym).method(action.to_sym) @output = @action.call end # These next few lines are working on the assumption that if the file exists we want it, and the precompiler is # counting on that. output_full_path = asset @output @output = File.new(output_full_path) if File.exist?(output_full_path) && !Dir.exist?(output_full_path) @method += Array(args[:respond_to]) unless args[:respond_to].nil? @method -= Array(args[:ignore]) unless args[:ignore].nil? @status_code = args[:status] unless args[:status].nil? @status_code = args[:status_code] unless args[:status_code].nil? Compilation.precompile(self) @headers["content-type"] = MIME::Types.of(@filename)[0].to_s unless @filename.nil? @block = @actions unless @actions.nil? self end end module Helpers def http { request: @request, response: @response, view: @view, method: @method, headers: @headers, status_code: @status_code } end def params @request.params end def view @view 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 include Helpers include Compilation class << self attr_accessor :routes # An array of instances of {ATD::Route} that belong to this {App}. # 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 [:get, :post, :put, :patch, :delete].each do |i| define_method(i) do |*args, &block| request.send(i, *args, &block) # Makes get == r.get, post == r.post, etc. 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: new, server: server, Port: port) end 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 = []) @routes = (routes + Array(self.class.routes)).each do |route| Compilation.precompile(route) unless route.output.is_a? Hash end end # Allows instance method route creation. Just another way of creating routes. def request(*args, &block) route = ATD::Route.new(*args, &block) @routes += Array(route) route end alias req request alias r request # 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) routes = @routes.where(path: env["PATH_INFO"], method: env["REQUEST_METHOD"].downcase.to_sym) warn "WARNING: Multiple routes matched the request" if routes.length > 1 route = routes.first return error 404 if route.nil? output = route.output output = Compilation.compile(route)[:content] unless route.args[:compile] == false return [route.status_code.to_i, Hash(route.headers), Array(output)] if route.block.nil? generate_variables(env, route) return_val = instance_eval(&route.block) if route.block.is_a? Proc return_val = method(route.block).call if route.block.is_a? Method @view[:raw] = return_val if @view[:raw].nil? || @view[:raw].empty? [@status_code.to_i, Hash(@headers), Array(@view[:raw])] end private def generate_variables(env, route) @status_code = 200 @headers = {} @view = { raw: route.output } @request = Rack::Request.new(env) @method = env["REQUEST_METHOD"] @response = Rack::Response.new(env) 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 # Starts the rack server # @param [Class] app The app you would like to start # @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(app = DefaultApp, server = WEBrick, port = 3150) Rack::Server.start(app: app.new, server: server, Port: port) end [:get, :post, :put, :patch, :delete].each do |i| define_method(i) do |*args, &block| request.send(i, *args, &block) end end Object.const_set(:DefaultApp, Class.new(ATD::App)) # Create DefaultApp