Phaedra: Serverless Ruby Functions

Phaedra is a web microframework for writing serverless Ruby functions. They are isolated pieces of logic which respond to HTTP requests (GET, POST, etc.) and typically get mounted at a particular URL path. They can be tested locally and deployed to a supported serverless hosting platform or to any Rack-compatible web server.

Serverless compatibility is presently focused on Vercel and OpenFaaS, but there are likely additional platforms we'll be adding support for in the future.

Installation

Add this line to your application's Gemfile:

gem "phaedra"

And then execute:

$ bundle

Or install it yourself as:

$ gem install phaedra

Usage

Functions are single Ruby files which respond to a URL path (aka /api/path/to/function). The path is determined by the location of the file on the filesystem relative to the functions root (aka api). So, given a path of ./api/folder/run-me.rb, the URL path would be /api/folder/run-me.

Functions are written as subclasses of Phaedra::Base using the name PhaedraFunction. The params argument is a Hash containing the parsed contents of the incoming query string, form data, or body JSON. The response object returned by your function is typically a Hash which will be transformed into JSON output automatically, but it can also be plain text.

Some platforms such as Vercel require the function class name to be Handler, so you can put that at the bottom of your file for full compatibility.

Here's a basic example:

require "phaedra"

class PhaedraFunction < Phaedra::Base
  def get(params)
    {
      text: "I am a response!",
      equals: params[:left].to_i + params[:right].to_i
    }
  end
end

Handler = PhaedraFunction

Your function can support get, post, put, patch, and delete methods which map to the corresponding HTTP verbs.

Each method is provided access to request and response objects. If your function was directly instantiated by WEBrick, those will be WEBrick::HTTPRequest and WEBrick::HTTPResponse respectively. If your function was instantiated by Rack, those will be Phaedra::Request (a thin wrapper around Rack::Request) and Rack::Response respectively.

Callbacks

Functions can define action callbacks:

class PhaedraFunction < Phaedra::Base
  before_action :do_stuff_before
  after_action :do_stuff_after
  around_action :do_it_all_around

  def do_stuff_before
    # code
  end

  # do_stuff_after, etc.

  def get(params)
    # this will be run within the callback chain
  end
end

You can modify the request object in a before_action callback to perform setup tasks before the actions are executed, or you can modify response in a after_action to further process the response.

OpenFaaS

We recommend using OpenFaaS' ruby-http template. It boots up a Sinatra/WEBrick server and then passes all requests along to a Handler object.

In your OpenFaaS project's function folder (e.g., testphaedra), simply define a file handler.rb which will load Phaedra's default Rack app:

# testphaedra/handler.rb

require "phaedra"

class Handler
  def run(_body, env)
    status, headers, body = Phaedra::RackApp.new({
      "root_dir" => File.join(Dir.pwd, "function")
    }).call(env)

    # The OpenFaaS ruby-http return array is backwards from Rack :/
    [body.join(""), headers, status]
  end
end

Next, add a YAML file that lives alongside your function folder:

# testphaedra.yml

version: 1.0
provider:
  name: openfaas
  gateway: http://127.0.0.1:8080
functions:
  testphaedra:
    lang: ruby-http
    handler: ./testphaedra
    image: yourdockerusername/testphaedra:latest

Now run faas-cli up -f testphaedra.yml to build and deploy the function. Given the Ruby function testphaedra/api/run-me.rb, you'd call it like so:

curl http://127.0.0.1:8080/function/testphaedra/api/run-me

# output of the Ruby function

In case you're wondering: yes, with Phaedra you can write multiple Ruby functions accessible via different URL paths that will all get handled by a single OpenFaaS function. Obviously you're welcome to set up multiple Phaedra projects and deploy them as separate OpenFaaS functions if you wish.

Rack

Booting Phaedra up as Rack app is very simple. All you need is a config.ru file alongside your api folder:

require "phaedra"

run Phaedra::RackApp.new

Then run rackup in the terminal.

WEBrick

Integrating Phaedra into a WEBrick server is pretty straightforward. Given a server object, it can be accomplished thusly:

full_api_path = File.expand_path("api", Dir.pwd)
base_api_folder = File.basename(full_api_path)

server.mount_proc "/#{base_api_folder}" do |req, res|
  api_folder = File.dirname(req.path).sub("/#{base_api_folder}", "")
  endpoint = File.basename(req.path)
  ruby_path = File.join(full_api_path, api_folder, "#{endpoint}.rb")

  if File.exist?(ruby_path)
    if Object.constants.include?(:PhaedraFunction)
      Object.send(:remove_const, :PhaedraFunction)
    end
    load ruby_path

    func = PhaedraFunction.new
    func.service(req, res)
  else
    raise HTTPStatus::NotFound, "`#{req.path}' not found."
  end
end

You also have the option of loading and mounting Handler directly to the server:

load File.join(Dir.pwd, "api", "func.rb")
@server.mount '/path', Handler

Development

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/whitefusionhq/phaedra.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Phaedra project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.