hybridgroup/artoo

View on GitHub
lib/artoo/api/route_helpers.rb

Summary

Maintainability
C
1 day
Test Coverage
module Artoo
  module Api
  # Route helpers used within the Artoo::Api::Server class
    module RouteHelpers
      class ResponseHandled < StandardError; end
      module ClassMethods

        # Path to api/public directory
        # @return [String] static path
        def static_path(default = Gem.loaded_specs['robeaux'].full_gem_path)
          @static_path ||= default
        end

        # @return [Hash] routes
        def routes
          @routes ||= {}
        end

        # Adds compiled signature to routes hash
        # @return [Array] signature
        def route(verb, path, &block)
          signature = compile!(verb, path, block, {})
          (routes[verb] ||= []) << signature
        end

        # Creates method from block, ripped from Sinatra
        # 'cause it's so sexy in there
        # @return [Method] generated method
        def generate_method(method_name, &block)
          define_method(method_name, &block)
          method = instance_method method_name
          remove_method method_name
          method
        end

        #@todo Add documentation
        def compile!(verb, path, block, options = {})
          options.each_pair { |option, args| send(option, *args) }
          method_name             = "#{verb} #{path}"
          unbound_method          = generate_method(method_name, &block)
          pattern, keys           = compile path
          conditions, @conditions = @conditions, []

          [ pattern, keys, conditions, block.arity != 0 ?
              proc { |a,p| unbound_method.bind(a).call(*p) } :
              proc { |a,p| unbound_method.bind(a).call } ]
        end

        #@todo Add documentation
        def compile(path)
          keys = []
          if path.respond_to? :to_str
            ignore = ""
            pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) do |c|
              ignore << escaped(c).join if c.match(/[\.@]/)
              patt = encoded(c)
              patt.gsub(/%[\da-fA-F]{2}/) do |match|
                match.split(//).map {|char| char =~ /[A-Z]/ ? "[#{char}#{char.tr('A-Z', 'a-z')}]" : char}.join
              end
            end
            pattern.gsub!(/((:\w+)|\*)/) do |match|
              if match == "*"
                keys << 'splat'
                "(.*?)"
              else
                keys << $2[1..-1]
                ignore_pattern = safe_ignore(ignore)

                ignore_pattern
              end
            end
            [/\A#{pattern}\z/, keys]
          elsif path.respond_to?(:keys) && path.respond_to?(:match)
            [path, path.keys]
          elsif path.respond_to?(:names) && path.respond_to?(:match)
            [path, path.names]
          elsif path.respond_to? :match
            [path, keys]
          else
            raise TypeError, path
          end
        end

        #@todo Add documentation
        def safe_ignore(ignore)
          unsafe_ignore = []
          ignore = ignore.gsub(/%[\da-fA-F]{2}/) do |hex|
            unsafe_ignore << hex[1..2]
            ''
          end
          unsafe_patterns = unsafe_ignore.map do |unsafe|
            chars = unsafe.split(//).map do |char|
              if char =~ /[A-Z]/
                char <<= char.tr('A-Z', 'a-z')
              end
              char
            end

            "|(?:%[^#{chars[0]}].|%[#{chars[0]}][^#{chars[1]}])"
          end
          if unsafe_patterns.length > 0
            "((?:[^#{ignore}/?#%]#{unsafe_patterns.join()})+)"
          else
            "([^#{ignore}/?#]+)"
          end
        end

        # Route function for get
        def get(path, &block)
          route 'GET', path, &block
        end

        # Route function for get_ws
        def get_ws(path, &block)
          route 'GET', path, &block
        end

        # Route function for post
        def post(path, &block)
          route 'POST', path, &block
        end

        # Route function for put
        def put(path, &block)
          route 'PUT', path, &block
        end

        # Route function for put
        def any(path, &block)
          route 'GET', path, &block
          route 'POST', path, &block
        end
      end

      module InstanceMethods
        ## Handle the request
        def dispatch!(connection, req)
          resp = catch(:halt) do
            try_static! connection, req
            route!      connection, req
          end

          return unless connection.response_state == :headers

          if resp && !resp.nil?
            status, body = resp

            begin
              if @is_static
                req.respond status, body
              else
                req.respond status, {'Content-Type' => 'application/json'}, body
              end
            rescue Errno::EAGAIN
              retry
            end
          else
            @error ||= "NOT FOUND"
            req.respond :not_found, {'Content-Type' => 'application/json'}, {error: @error}.to_json
            @error = nil
          end
        end

        # Exit the current block, halts any further processing
        # of the request, and returns the specified response.
        def halt(*response)
          response = response.first if response.length == 1
          throw :halt, response
        end

        def try_static!(connection, req)
          fpath = req.url == '/' ? 'index.html' : req.url[1..-1]
          filepath = File.expand_path(fpath, self.class.static_path)
          if File.file?(filepath)
            # TODO: stream this?
            data = open(filepath).read
            @is_static = true
            halt :ok, data
          end
          @is_static = false
        end

        def route!(connection, req)
          if routes = self.class.routes[req.method]
            routes.each do |pattern, keys, conditions, block|
              route = req.url
              next unless match = pattern.match(route)
              values = match.captures.to_a.map { |v| URI.decode_www_form_component(v) if v }
              if values.any?
                params = {}
                keys.zip(values) { |k,v| Array === params[k] ? params[k] << v : params[k] = v if v }
                @params = params
              end

              @connection = connection
              @req = req

              begin
                body = block ? block[self, values] : yield(self, values)
                halt [:ok, body]
              rescue Exception => e
                p [:e, e]
              end
            end
          end
          nil
        end

        # Fixes encoding issues by
        # * defaulting to UTF-8
        # * casting params to Encoding.default_external
        #
        # The latter might not be necessary if Rack handles it one day.
        # Keep an eye on Rack's LH #100.
        def force_encoding(*args) settings.force_encoding(*args) end
        if defined? Encoding
          def self.force_encoding(data, encoding = default_encoding)
            return if data == settings || data.is_a?(Tempfile)
            if data.respond_to? :force_encoding
              data.force_encoding(encoding).encode!
            elsif data.respond_to? :each_value
              data.each_value { |v| force_encoding(v, encoding) }
            elsif data.respond_to? :each
              data.each { |v| force_encoding(v, encoding) }
            end
            data
          end
        else
          def self.force_encoding(data, *) data end
        end
      end

      def self.included(receiver)
        receiver.extend         ClassMethods
        receiver.send :include, InstanceMethods
      end
    end
  end
end