Shojiku for Ruby
Ruby bindings for Shojiku — a document engine that turns a YAML template plus your data into a deterministic PDF, signs it, and verifies it.
Unreleased. The gem is written and gated but not published yet — all seven Shojiku SDKs publish together at v0.1.0. Until then, install from a repository clone (see Building the engine library below), or use the CLI or the Docker image — see the quickstart.
Install
bundle add shojiku
Platform gems carry a prebuilt engine library, so there is no build step
on the supported platforms (Linux and macOS on x86-64 and arm64, Windows
on x86-64). The engine is loaded through fiddle from the standard
library rather than as a native extension, which means one binary serves
every supported Ruby — no recompile when you upgrade the interpreter.
Usage
require "shojiku"
client = Shojiku::Client.new(templates: "app/templates")
result = client.generate("receipt_ja", {
customer: { name: "Yamada Shoji K.K." },
items: [{ name: "Consulting", qty: 1, price: 120_000 }],
})
if result.success?
result.artifact.write("receipt.pdf")
else
result.failure.causes.each { |cause| warn cause.to_s }
end
params is a Hash, or a String you already have — JSON or YAML, since
the engine parses either and a String is passed through verbatim. There
is no per-format method: format dispatch is the engine's.
To render one template in several locales, derive a client:
client.with_lang("ja-JP").generate("receipt_ja", params)
A derived client shares the loaded engine — deriving re-opens nothing — and the locale it names beats the one the client was built with.
Sources you already hold
generate resolves a template name against the configured root.
When the sources come from somewhere else — object storage, a database,
a heredoc — hand them over directly:
result = client.generate_source(
template: File.read("invoice.yml"), # source text, never a path
definitions: definitions_yaml, # optional
assets_dir: "app/templates/invoice", # optional; bundled images resolve here
params: params
)
Fetching the bytes stays your application's act: this gem opens no
sockets and reads no template off disk here. template: is source
text — a value that looks like a path is a template that fails to
parse, not a file this gem will open — so the containment rules that
guard the name entrance are not being bypassed, they are simply not
involved. A deployment that wants to forbid this entrance entirely
declares strict:.
An archived document goes back in the same way:
archived = client.artifact(File.binread("2026-invoices/0042.pdf"))
archived.verify(anchors: "ca.crt")
Signing is a separate step over the rendered document, and verification is a separate step over that:
signed = result.artifact.sign(
Shojiku::LocalPem.new(key: "signer.key", cert: "signer.crt")
)
checked = signed.artifact.verify(anchors: "ca.crt")
checked.report.valid? # every check this release performs passed
checked.report.not_checked # => [:revocation, :timestamp]
Read not_checked beside the verdict. valid? means "nothing we
looked at was wrong", not "this document is trustworthy" — revocation
and timestamping are not checked in this release, and the report says so
on a passing verdict as well as a failing one. A document whose
signature does not verify comes back as a failed result that still
carries the full report.
Results, not exceptions
Nothing raises in the normal flow. Every operation returns a result you
query — success?, the artifact or report, and the engine's diagnostics
either way (a render that worked can still have warned about an
overflowing box). A failure carries a trace: which step failed, a stable
kind, the message, and the cause chain underneath it.
result.warnings.each { |d| logger.warn("#{d.code}: #{d.}") }
result.failure.step # => :generate
result.failure.kind # => "template_not_found"
result.failure.causes # => [the failure, its cause, …]
Diagnostics keep the engine's stable code and typed args untouched,
so an application that renders its own localized messages has everything
it needs. This gem never translates them.
Exceptions are reserved for what Ruby reserves them for: programmer
misuse (Shojiku::UsageError) and an environment with no engine in it
(Shojiku::LibraryNotFound).
For a script that would rather have a stack trace than a branch, there is an opt-in unwrap:
client.generate("receipt_ja", params).artifact!.write("receipt.pdf")
artifact! / report! raise Shojiku::UnwrapError (carrying the whole
failure) when the result failed. The ruling behind them is deliberate
and shared by every Shojiku SDK: calling unwrap on a failed result is
programmer misuse — a caller who has not checked success? is
asserting the operation worked. Application code that handles failure
keeps using success?.
Templates
Template names are identifiers, never paths. A name resolves against the configured template root:
app/templates/
receipt_ja/
templates.yml (required)
definitions.yml (optional)
assets/ (optional — bundled images resolve against it)
Names containing a path separator, a .. segment, a drive letter, a UNC
prefix, a control character, or a reserved Windows device name (CON,
NUL, COM1, …) are refused — on every platform, not only the ones
where they would resolve, so a name that works in development works in
production. A name that resolves outside the root through a symlink is
refused after canonicalization.
Every one of those is a failed result: a hostile name is a fact about the request, not a bug in your program. A name that is not a String at all is the other thing, and raises.
Configuration
Shojiku::Client.new(
templates: "app/templates", # the template root
font_dirs: ["vendor/fonts"], # extra font-pack search directories
locale_dirs: ["vendor/locales"],
lang: "ja-JP", # overrides the template's own default
library: "/opt/lib/libshojiku_capi.so",
logger: Rails.logger, # optional; host events only (see Logging)
strict: true, # the input ceiling (see below)
providers: { invoice: … }, # signing providers, by name
env: true # consult SHOJIKU_* variables at all
)
| Variable | What it sets |
|---|---|
SHOJIKU_TEMPLATE_ROOT |
the template root |
SHOJIKU_FONT_DIR |
font-pack directories (PATH-separated) |
SHOJIKU_LOCALE_DIR |
locale-pack directories (PATH-separated) |
SHOJIKU_LIBRARY |
the engine library to load |
env: false disables all of them at once, for an application that
wants its configuration to come from nowhere but its own code.
The same settings can be defaults for every client, in the idiom an initializer expects:
Shojiku.configure do |config|
config.templates = "app/templates"
config.lang = "ja-JP"
config.logger = Rails.logger
end
This sets defaults for the constructor; it does not add a precedence
layer. An explicit argument still wins, and a configured value still
wins over the environment — with strict: the one deliberate exception
(below). Shojiku.reset_configuration! drops the lot, which is what a
test suite wants between examples.
Locking down where signable input comes from
Once you sign what you render, template input is a security boundary: whoever controls the bytes controls what gets signed. An operator can declare a ceiling:
Shojiku.configure do |config|
config.strict = true
config.providers = { invoice: Shojiku::LocalPem.new(key: "signer.key", cert: "signer.crt") }
end
client.sign(document, :invoice) # by name; the key path is not here
A strict client:
- refuses
generate_source, so every document it signs came from the template root and its containment rules; - signs only a document it rendered from that root — bytes handed over
whole (
client.artifact) and bytes laid out from a caller's own template are the same trust class, and signing does not launder them; - takes signing material only as the name of a provider registered in configuration, so a key path never appears in request-handling code and the material is loaded by one object.
Verification is never restricted. Verifying bytes of unknown provenance is the point of verify, and a locked-down deployment is exactly the one that has to check an archived document it did not produce.
Refusals raise Shojiku::UsageError rather than returning a failed
result: strict disables an entrance, so calling it is the program
contradicting its own deployment, not a fact about a document — and a
failed result is something if result.success? can swallow. Strict is
also the one setting Shojiku.configure wins outright: a restriction an
operator declared must not be lifted by Client.new(strict: false).
Logging
Optional, silent by default, and narrow on purpose:
Shojiku::Client.new(templates: "app/templates", logger: Rails.logger)
Any object answering debug is accepted (this gem takes no logger
dependency). It reports what the binding did — which library it
loaded and why that one, the ABI revision it found, which lifecycle step
ran, for how long, and whether it worked. It never logs params, document
bytes, key material, a passphrase, or the engine's diagnostics: a log
line is the easiest way for a secret to leave a process, and the
diagnostics already belong to the result you are holding.
Threads
A render, a signature and a verification each release Ruby's global VM
lock for the duration of the call into the engine, so a long render does
not block other threads in the process. Client is safe to share across
threads, and the engine library itself may be called concurrently — the
same params produce the same bytes whether one thread is rendering or
eight are.
Explicit configuration wins for the template root and the pack
directories — what an application renders is the application's own
decision. SHOJIKU_LIBRARY is the one that goes the other way and beats
an explicit library:, because where the engine lives is a deployment
decision that has to be able to override application code. That is the
same order SHOJIKU_BIN has in the SDKs that shell out to the CLI.
Nothing in this gem downloads anything, at install time or at run time.
Building the engine library
Until the platform gems are published, build the library from a clone:
make capi-lib
It lands in dist/capi/local/; point SHOJIKU_LIBRARY at it, or pass
library: to the client.
Development
Every gate runs in a container — no Ruby toolchain needed locally:
make verify:sdk:ruby
make test:sdk:ruby and make lint:sdk:ruby are the faster slices.
Requirements
Ruby 3.3 or newer.
Documentation
- Template reference — how to write the YAML the engine renders
- SDK policy — the lifecycle contract every Shojiku SDK implements
License
Licensed under any of Apache-2.0, MIT, or BSD-3-Clause, at your option.