lib/atd.rb in atd-0.4.0 vs lib/atd.rb in atd-0.5.0

- old
+ new

@@ -1,15 +1,19 @@ 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) @@ -18,15 +22,20 @@ 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, :actions, :status_code + 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 @@ -69,10 +78,11 @@ 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 @@ -83,71 +93,69 @@ @method.uniq! parse_args(*args, &block) 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] = { - status_code: @status_code, - output: @output, - block: @block, - args: @args, - route: self - } - routes - end - private # This should also manage @method at some point - def parse_args(*args, &block) - args.compact! - args.flatten! - args.reject! { |arg| arg.is_a?(File) || arg.is_a?(Proc) || arg ? false : arg.empty? } # File doesn't respond to empty - @block = block - # This requires the format ATD::Route.new(path, route, args) - @path ||= args.shift - @output ||= args.shift - @args = Array(@args).concat(args) unless args.nil? + 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 - # TODO: Choose one! They all work... I think... - # Method 1: - target_location = [] - caller_locations.each do |caller_location| - target_dir = File.dirname(caller_location.absolute_path.to_s) - target_location.push(target_dir) unless target_dir.include?(__dir__) - end - # Method 2: - target_location = caller_locations.reject do |caller_location| - File.dirname(caller_location.absolute_path.to_s).include? __dir__ - end - output_full_path = "#{File.dirname(target_location[0].absolute_path)}/assets/#{@output}" + # 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) - if args.is_a?(Hash) || args.last.is_a?(Hash) - @method += Array(args.last[:respond_to]) unless args.last[:respond_to].nil? - @method -= Array(args.last[:ignore]) unless args.last[:ignore].nil? - @status_code = args.last[:status] unless args.last[:status].nil? - @status_code = args.last[:status_code] unless args.last[:status_code].nil? - end + @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 - attr_accessor :http + 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. @@ -190,34 +198,19 @@ # } # } # } # @param [Array] routes An array of instances of {ATD::Route}. def initialize(routes = []) - @routes = {} - Array(routes + self.class.routes).each do |route| - route = route.clone - filename = ATD::Compilation.precompile(route, (route.args.last.is_a?(Hash) ? route.args.last[:precompile] : nil)) - route_hash = route.to_h - current_route = route_hash[route.path][route.method] - current_route[:filename] = filename - block = current_route[:block] - # An instance method must be defined from the block make it the same as the controller actions. We don't want to - # convert the controller actions to blocks because if we did that, we would have to take them out of scope to allow - # them to use the @http variables. - current_route[:block] = define_singleton_method(block.object_id.to_s.tr("0-9", "a-j").to_sym, &block) unless block.nil? - current_route[:block] = route.actions unless route.actions.nil? - @routes = @routes.to_h.deep_merge(route_hash) + @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) - filename = ATD::Compilation.precompile(route, (route.args.last.is_a?(Hash) ? route.args.last[:precompile] : nil)) - route_hash = route.to_h - route_hash[route.path][route.method][:filename] = filename - @routes = @routes.to_h.deep_merge(route_hash) + @routes += Array(route) route end alias req request alias r request @@ -230,42 +223,44 @@ # 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) - @http = nil - route = route(env) - return error(404) if route.nil? - route[:output] = Compilation.compile(route[:filename], route[:output]) unless !route[:args].nil? && !route[:args].empty? && route[:args][0].is_a?(Hash) && route[:args][0][:compile] == false - 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"], response: Rack::Response.new(env) - return_val = method(route[:block]).call - @http[:output] = return_val if @http[:output].nil? - [@http[:status_code].to_i, Hash(@http[:headers]), Array(@http[:output])] + 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 route(env) - return nil if @routes[env["PATH_INFO"]].nil? - # return @routes[env["PATH_INFO"]][[]] unless @routes[env["PATH_INFO"]][[]].nil? - @routes[env["PATH_INFO"]].include_in_key?(env["REQUEST_METHOD"].downcase.to_sym) + 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 http(additional_params) - @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) + ATD::App.request(*args, &block) end alias req request alias r request # Starts the rack server