#!/usr/bin/env ruby require 'sinatra' require 'sinatra/multi_route' require 'yaml' require 'oj' require 'faastruby-rpc' require 'base64' module FaaStRuby class DoubleRenderError < StandardError; end end module FaaStRuby class Runner def initialize @rendered = false end def call(workspace_name, function_name, event, args) begin load "./#{workspace_name}/#{function_name}/handler.rb" response = handler(event, *args) return response if response.is_a?(FaaStRuby::Response) body = { 'error' => "Please use the helpers 'render' or 'respond_with' as your function return value." } FaaStRuby::Response.new(body: Oj.dump(body), status: 500, headers: {'Content-Type' => 'application/json'}) rescue Exception => e body = { 'error' => e.message, 'location' => e.backtrace&.first, } FaaStRuby::Response.new(body: Oj.dump(body), status: 500, headers: {'Content-Type' => 'application/json'}) end end def rendered! @rendered = true end def rendered? @rendered end def respond_with(body, status: 200, headers: {}, binary: false) raise FaaStRuby::DoubleRenderError.new("You called 'render' or 'respond_with' twice in your handler method") if rendered? response = FaaStRuby::Response.new(body: body, status: status, headers: headers, binary: binary) rendered! response end def render( js: nil, body: nil, inline: nil, html: nil, json: nil, yaml: nil, text: nil, data: nil, png: nil, svg: nil, jpeg: nil, gif: nil, icon: nil, status: 200, headers: {}, content_type: nil, binary: false ) headers["Content-Type"] = content_type if content_type bin = false case when json headers["Content-Type"] ||= "application/json" resp_body = json.is_a?(String) ? json : Oj.dump(json) when html, inline headers["Content-Type"] ||= "text/html" resp_body = html when text headers["Content-Type"] ||= "text/plain" resp_body = text when yaml headers["Content-Type"] ||= "application/yaml" resp_body = yaml.is_a?(String) ? yaml : YAML.load(yaml) when body headers["Content-Type"] ||= "application/octet-stream" bin = binary resp_body = bin ? Base64.urlsafe_encode64(body) : body when data headers["Content-Type"] ||= "application/octet-stream" resp_body = Base64.urlsafe_encode64(data) bin = true when js headers["Content-Type"] ||= "text/javascript" resp_body = js when png headers["Content-Type"] ||= "image/png" resp_body = Base64.urlsafe_encode64(png) bin = true when svg headers["Content-Type"] ||= "image/svg+xml" resp_body = svg when jpeg headers["Content-Type"] ||= "image/jpeg" resp_body = Base64.urlsafe_encode64(jpeg) bin = true when gif headers["Content-Type"] ||= "image/gif" resp_body = Base64.urlsafe_encode64(gif) bin = true when icon headers["Content-Type"] ||= "image/x-icon" resp_body = Base64.urlsafe_encode64(icon) bin = true end respond_with(resp_body, status: status, headers: headers, binary: bin) end end class Event < Struct end class Response attr_accessor :body, :status, :headers, :binary def initialize(body:, status: 200, headers: {}, binary: false) @body = body @status = status @headers = headers @binary = binary end def binary? @binary end end end class FaaStRubyServer < Sinatra::Application set :port, 3000 set :bind, '0.0.0.0' case ARGV.shift when '-p' set :port, ARGV.shift when '-b' set :bind, ARGV.shift end set :server, %w[puma] set :run, false set :show_exceptions, true register Sinatra::MultiRoute route :get, :post, :put, :patch, :delete, '/:workspace_name/:function_name' do e = FaaStRuby::Event.new(:body, :query_params, :headers, :context) headers = env.select { |key, value| key.include?('HTTP_') || ['CONTENT_TYPE', 'CONTENT_LENGTH', 'REMOTE_ADDR', 'REQUEST_METHOD', 'QUERY_STRING'].include?(key) } if headers.has_key?("HTTP_FAASTRUBY_RPC") body = nil rpc_args = parse_body(request.body.read, headers['CONTENT_TYPE'], request.request_method) || [] else body = parse_body(request.body.read, headers['CONTENT_TYPE'], request.request_method) rpc_args = [] end query_params = parse_query(request.query_string) context = set_context(params[:workspace_name], params[:function_name]) event = e.new(body, query_params, headers, context) response = FaaStRuby::Runner.new.call(params[:workspace_name], params[:function_name], event, rpc_args) status response.status headers response.headers if response.binary? body Base64.urlsafe_decode64(response.body) else body response.body end end def parse_body(body, content_type, method) return nil if method == 'GET' return {} if body.nil? && method != 'GET' return Oj.load(body) if content_type == 'application/json' return body end def set_context(workspace_name, function_name) return nil unless File.file?('context.yml') yaml = YAML.load(File.read('context.yml')) return nil unless yaml.has_key?(workspace_name) yaml[workspace_name][function_name] end def parse_query(query_string) hash = {} query_string.split('&').each do |param| key, value = param.split('=') hash[key] = value end hash end def self.run? true end end FaaStRubyServer.run! rescue nil # this will suppress some of the errors messages