Skip to content

Ruby / Rails

In this chapter, you’ll integrate Oicana into a Ruby web service using Ruby on Rails. Rails is the most widely used Ruby web framework, and its API mode is a lean setup for web services. We’ll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.

Let’s start with a fresh Rails project in API mode. Create it in a new directory, separate from your template directory:

Terminal window
rails new my-pdf-service --api
cd my-pdf-service

Create a controller with a basic action:

app/controllers/hello_controller.rb
class HelloController < ApplicationController
def index
render plain: "Hello World"
end
end

Then route the root path to it by adding a line to config/routes.rb. Keep the up route that Rails generated, the health check of the generated Docker and Kamal setup:

config/routes.rb
Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check
get "/", to: "hello#index"
end

You can test it by running bin/rails server and navigating to http://localhost:3000 in your browser. You should see “Hello World”.

We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.

  1. Create a new directory in the Rails project called templates and copy example-0.1.0.zip into that directory.

  2. Add the oicana gem as a dependency with bundle add oicana.

  3. Define the template in an initializer. It is created once per process, on first use:

    config/initializers/oicana.rb
    module ExampleTemplate
    MUTEX = Mutex.new
    def self.instance
    MUTEX.synchronize do
    @instance ||= Oicana::Template.new(Rails.root.join("templates/example-0.1.0.zip").binread)
    end
    end
    end
  4. Replace the hello controller with a controller for the compile endpoint. You can delete app/controllers/hello_controller.rb.

    app/controllers/documents_controller.rb
    class DocumentsController < ApplicationController
    def compile
    pdf = ExampleTemplate.instance.export_pdf(mode: :development)
    send_data pdf, type: "application/pdf", filename: "example.pdf"
    end
    end
  5. Replace the root route with a route to the new action:

    config/routes.rb
    Rails.application.routes.draw do
    get "up" => "rails/health#show", as: :rails_health_check
    post "/compile", to: "documents#compile"
    end

    The first request loads the template, and every later request of the same process reuses it. The /compile endpoint compiles the template and returns the PDF file. We explicitly pass the compilation mode with export_pdf(mode: :development) so the template uses the development value you defined for the info input ({ "name": "Chuck Norris" }). In a follow-up step, we will set an input value instead.

After restarting the server with bin/rails server, you can test the endpoint with curl:

Terminal window
curl -X POST http://localhost:3000/compile --output example.pdf

The generated example.pdf file should contain your template with the development value.

The PDF generation should not take longer than a couple of milliseconds.

Puma, the Rails application server, handles requests on a pool of threads. A Template is safe to use from multiple threads, and Oicana releases the GVL while it compiles, so the other threads keep serving requests in the meantime. Compilations of the same Template instance serialize internally. For more concurrency under heavy load, create multiple Template instances from the same template file.

Repeated compilations are fast because Typst memoizes its work in a global cache. The cache management guide explains how that cache is evicted and when it pays off to tune it.

Our compile action is currently calling export_pdf with development mode. Now we’ll provide explicit input values and switch to production mode:

app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
def compile
pdf = ExampleTemplate.instance.export_pdf(json_inputs: { "info" => { name: "Baby Yoda" } })
send_data pdf, type: "application/pdf", filename: "example.pdf"
end
end

JSON input values can be Hashes and any other value JSON.generate accepts, or Strings that already hold JSON. To pass a JSON string value, encode it first, for example "Baby Yoda".to_json.

Your explicit input value takes precedence over the development value in either mode, so it is what changes the output here. Notice that we removed the explicit mode: :development argument. export_pdf defaults to :production when no mode is specified. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles none values for that input.

Calling the endpoint now will result in a PDF with “Baby Yoda” instead of “Chuck Norris”. Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the open source Rails example project on GitHub for a more complete showcase of the Oicana Ruby integration, including blob inputs, error handling, and request validation.

For inputs other than JSON, see Template inputs, which documents blob inputs with examples for every integration.

A missing required input or an input that fails schema validation makes the compilation fail. The tutorial’s example template declares no JSON schema, so only the first case can happen here. Oicana raises subclasses of Oicana::Error, which a controller can handle with rescue_from:

app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
rescue_from Oicana::Error do |error|
Rails.logger.error("Failed to compile template: #{error.message}")
render plain: "Failed to generate the document", status: :internal_server_error
end
def compile
pdf = ExampleTemplate.instance.export_pdf(json_inputs: { "info" => { name: "Baby Yoda" } })
send_data pdf, type: "application/pdf", filename: "example.pdf"
end
end

To answer invalid request payloads with HTTP 400 instead, rescue Oicana::InvalidInputError separately. It covers inputs that fail schema validation or that the template does not declare. A missing required input fails the compilation itself and raises Oicana::CompilationError.

Complete code at the end of this chapter
config/initializers/oicana.rb
module ExampleTemplate
MUTEX = Mutex.new
def self.instance
MUTEX.synchronize do
@instance ||= Oicana::Template.new(Rails.root.join("templates/example-0.1.0.zip").binread)
end
end
end
app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
rescue_from Oicana::Error do |error|
Rails.logger.error("Failed to compile template: #{error.message}")
render plain: "Failed to generate the document", status: :internal_server_error
end
def compile
pdf = ExampleTemplate.instance.export_pdf(json_inputs: { "info" => { name: "Baby Yoda" } })
send_data pdf, type: "application/pdf", filename: "example.pdf"
end
end
config/routes.rb
Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check
post "/compile", to: "documents#compile"
end