fbpx

[Crystal] Getting Started

Introduction

FaaStRuby is a serverless platform built for Ruby developers. You deploy functions to workspaces and trigger them via HTTP endpoints.

The platform currently supports Crystal versions 0.27.0 and 0.27.2.

Workspaces

Workspaces are groups of functions and can be used to mimic environments or to compose a set of routes for a distributed app based on functions. They are represented by the URL https://api.tor1.faastruby.io/WORKSPACE_NAME. Note that workspace names must be unique.
When you create a workspace you receive an API key/secret pair. You need to use those credentials to make changes to the workspace so store them somewhere safe.

You can upload functions to any existing workspaces you own. Functions can be triggered through their workspace URL. For example, a function named ‘slack-bot’ in the workspace ‘catops-prod’ will have the URL https://api.tor1.faastruby.io/catops-prod/slack-bot.

Creating a Workspace

The first thing you need to do is create a workspace. The command below will send a request to create a workspace ‘catops-prod’ on FaaStRuby and write the credentials to ‘~/.faastruby.tor1’, where ‘tor1’ is the region code for Toronto, Canada.

~$ faastruby create-workspace catops-prod
◐ Requesting credentials... Done!
Writing credentials to /Users/demo/.faastruby.tor1
~ f /Users/demo/.faastruby.tor1
Workspace 'catops-prod' created

If you want to print the credentials to STDOUT instead of saving it to a file, use the option –stdout when creating the workspace:

~$ faastruby create-workspace catops-prod --stdout
◐ Requesting credentials... Done!
IMPORTANT: Please store the credentials below in a safe place. If you lose them you will not be able to manage your workspace.
API_KEY: 63a02778c9342993801936ef4a87412e
API_SECRET: voeIjDPY5sloCZ0oo68nIw==
Workspace 'catops-prod' created

You can also attach an email address when creating a workspace. Although this is not required, it’s a good idea so you can be contacted in case of any problems with your workspace. To create a workspace attaching an email address to it, run:

~$ faastruby create-workspace catops-prod -e you@example.com

Your first function

Let’s create and deploy your first function. The function will take a JSON payload {“name”: “Crystal”} and respond with the string “Hello, Crystal!”. Let’s call it ‘hello-world’:

~$ cd catops-prod
~/catops-prod$ faastruby new hello-world --runtime crystal:0.27.0
+ d ./hello-world
+ d ./hello-world/spec    # Put your tests in here
+ d ./hello-world/spec/helpers    # Spec helpers go here
+ f ./hello-world/spec/helpers/faastruby.cr    # FaaStRuby::SpecHelper
+ f ./hello-world/spec/handler_spec.cr    # Spec for handler.cr
+ f ./hello-world/spec/spec_helper.cr    # Spec helper
+ f ./hello-world/shard.yml    # Just a regular Shards file
+ f ./hello-world/src/handler.cr    # The function handler. Must define a method 'handler'
+ f ./hello-world/faastruby.yml    # Your function configuration
◑ Installing shards... Done!

Let’s take a look at ‘faastruby.yml’

---
name: hello-world    # The function name
runtime: crystal:0.27.0  # The function runtime
test_command: crystal spec --no-color    # The command to run tests
abort_build_when_tests_fail: true    # Abort package build when tests fail
abort_deploy_when_tests_fail: true    # Abort deploy when tests fail

Now let’s look inside the file src/handler.cr. This is the file that is required by a runner when your function gets invoked. Note that the function returns the value from a ‘render’ call.

# require "cool-shard"
require "json"

# To deploy this function, cd into its folder and run:
# faastruby deploy-to WORKSPACE_NAME
def handler(event : FaaStRuby::Event) : FaaStRuby::Response
  # event.body : String | Nil
  # event.headers : Hash(String, String)
  # event.context : String | Nil
  # query_params : Hash(String, String)

  # FUNCTION RESPONSE
  #
  # You can render text, json, yaml, html or js. Example:
  # render html: "<p>Hello World!</p>"
  # render yaml: {"hello" => "world!"}
  #
  # Status:
  # The default status is 200. You can set a custom status like this:
  # render json: {"error" => "Could not perform the action"}, status: 422
  #
  # Headers:
  # The 'Content-Type' header is automatically set when you use 'render'.
  # You can set custom headers using a Hash(String, String). Example:
  # render text: "It Works!", headers: {"TransactionId" => 23928}
  render text: "Hello, World!\n"
end

You can call render with the keys text:, json:, js:, html:, yaml:, and body:. This method will set the Content-Type header automatically for you, and if you use the keyword argument body:, the Content-Type header will be set to “application/octet-stream”

You can also set the response status code and headers. The status code defaults to 200, and the headers parameter must be a Hash(String, String).

def handler(event : FaaStRuby::Event) : FaaStRuby::Response
  render text: "Hello, World!", status: 200, headers: {"Content-Type" => "text/plain"}
end

Now let’s update the function handler so it will get the request body, parse it and respond with “Hello, _____!” or “Hello, World!” if no name is present.

The ‘event’ parameter

The function handler takes a parameter ‘event’. This parameter is a Struct with the following attributes:

  • event.body – The request body string. It is up to you to parse it. Ex: JSON.parse(event.body)
  • event.query_params – A Hash with the URL query parameters. Ex: for ‘foo=bar&lorem=ipsum’, event.query_params[‘foo’] #=> “bar” and event.query_params[‘lorem’] #=> “ipsum”
  • event.headers – A hash with the request headers.
  • event.context – The execution context. Learn more.

Let’s say we decide to send a JSON payload to the function with the following content:

{"name": "Crystal"}

Then we parse ‘event.body’ and modify the function response. Here is the code:

require "json"

def handler event
  name = event.body ? JSON.parse(event.body.not_nil!)["name"] : "World"
  render text: "Hello, #{name}!\n"
end

Can’t forget to write some tests! 🙂
Here is the content for ‘spec/handler_spec.cr’:

require "./spec_helper"

describe "handler(event)" do
  body = {"name" => "Ruby"}.to_json
  event_hash = {
    "body" => body,
    "context" => nil,
    "headers" => {"Content-Type" => "application/json"},
    "query_params" => {} of String => String
  }
  event = Event.from_json(event_hash.to_json)

  it "should return String" do
    body = handler(event).body
    body.class.should eq(String)
  end
  it "should add the name to the response string" do
    body = handler(event).body
    body.should eq("Hello, Ruby!\n")
  end
  it "should say Hello, World! when name is not present" do
    event_hash["body"] = nil
    event = Event.from_json(event_hash.to_json)
    body = handler(event).body
    body.should eq("Hello, World!\n")
  end
end

Now let’s deploy this function to the FaaStRuby servers. You need to specify the workspace in which your function will be deployed. Let’s use our previously created ‘catops-prod’ workspace.

~/catops-prod$ cd hello-world
~/catops-prod/hello-world$ faastruby deploy-to catops-prod
◐ Running tests... Passed!
...

Finished in 0.00519 seconds (files took 0.14255 seconds to load)
3 examples, 0 failures

◐ Building package... Done!
◒ Deploying to workspace 'catops-prod'... Done!
Endpoint: https://api.tor1.faastruby.io/catops-prod/hello-world

Here is what happened:

  1. The tests run locally.
  2. A deployment package was built with the contents of the hello-world function.
  3. The package was uploaded to FaaStRuby.
  4. On the server side, the function is unpacked and the shards are installed.
  5. The tests run again on the server to make sure the functions will work once deployed.

Time to test it out by invoking the function with curl:

~/catops-prod/hello-world$ curl https://api.tor1.faastruby.io/catops-prod/hello-world
Hello, World!

It works! Now let’s try again passing a name via JSON body:

~/catops-prod/hello-world$ curl -X POST -H 'Content-Type: application/json' -d '{"name":"Ruby"}' 'https://api.tor1.faastruby.io/catops-prod/hello-world'
Hello, Crystal!

Writing temporary files

For security reasons, FaaStRuby functions cannot write to disk. You can, however, create temporary files up to 5MB of size. Example:

def handler(event : FaaStRuby::Event) : FaaStRuby::Response
  tempfile = File.tempfile("foo")
  # ...
end

Execution Context

Sometimes you want your function to know about some data that you don’t want to commit to source control or pass it on each request. With contexts you can have pre-loaded data available to your function on every run.

Execution context data is encrypted at rest, decrypted at runtime and passed to your function via ‘event.context’.

To add an execution context to a function, you use the ‘update-context’ command and pass the function’s workspace name:

~/catops-prod/hello-world$ faastruby update-context catops-prod --data '{"super_secret":"abc123"}'
◐ Uploading context data to 'catops-prod'... Done!

You can also read from stdin:

~/catops-prod/hello-world$ echo '{"super_secret":"abc123"}' | faastruby update-context catops-prod --stdin

Every time you run this command, the previous data is replaced with the new one. The data is handed to your function as-is, so it’s up to you to parse it within your function.

require "json"

def handler(event : FaaStRuby::Event) : FaaStRuby::Response
  context = JSON.parse(event.context)
  # context["super_secret"] #=> "abc123"
  render json: event.context.to_json # will print back the context
end

Custom responses: HTML, JSON, YAML, etc

If you need to respond with JSON, HTML or YAML, all you need to do is pass the correct keyword argument to the method render, and the response headers will be set for you. When using JSON or YAML, you have to make sure you serialize the object before passing it. Example:

def handler(event : FaaStRuby::Event) : FaaStRuby::Response
  response = {"foo" => "bar", "far" => "boo"}
  render yaml: response.to_yaml
end

The “render” method will automatically set the correct headers:

  • json: application/json
  • text: text/plain
  • yaml: text/yaml
  • html: text/html
  • js: text/javascript
  • body: application/octet-stream

Scheduling functions

You can schedule your functions to run periodically or at any time in the future. Functions can run on multiple schedules, and you configure them inside “faastruby.yml”. Example:

...
schedule:
  job1:
    when: every 2 hours
    body: {"foo": "bar"}
    method: POST
    query_params: {"param": "value"}
    headers: {"Content-Type": "application/json"}
  job2:
...

The schedule configuration takes the following keys:

when
Accepts plain English or the Cron syntax. Examples:

  • every 3 hours
  • 0 14 * * *
  • every day at 2am
  • every Tuesday at noon
  • every 5 days
  • every Sunday at 23:00

body
The request body to be passed to your function. Defaults to nil.

method
The request method to be used. Defaults are: GET when “body” is nil, and POST when “body” is not nil. Available as “event.body” within the function.

query_params
The URL query params to be passed to your function, in JSON format. Default to {}.

headers
The headers to be passed to your function. They will be available as “event.headers” and defaults to {}.
IMPORTANT: If you don’t specify a “Content-Type” header, the body will be processed as form data (key=value pairs).

Calling a function from within a function

Not implemented yet.

Creating a function from a template

Introduced with version 0.4.16.

You have the option to use a template when creating a function. You can use templates from the local disk or a remote Git repository. The syntax is:

~$ faastruby new FUNCTION_NAME --template TYPE(local|git|github):SOURCE

Example:

# Use a template from a Github repository:
~$ faastruby new my-function --template github:faastruby/hello-world-crystal-template

# Use a template from a Git repository:
~$ faastruby new my-function --template git:git@github.com:FaaStRuby/hello-world-crystal-template.git

# Use a template from the local disk:
~$ faastruby new my-function --template local:/path/to/template