Skip to content
Prompting Press v0.2

Getting started — Rust

Add the crate to Cargo.toml:

[dependencies]
prompting-press = "0.1"
garde = { version = "0.23", features = ["derive", "serde"] }
serde = { version = "1", features = ["derive"] }

garde is the Rust validator crate; serde is required so the Vars struct can be serialized across the FFI bridge.

A prompt definition is a YAML, JSON, or TOML document — the same shape in any of the three formats. The caller reads the text and hands it to the from_* factories; the library does no file I/O. The assistant definition used throughout this page:

assistant.yaml
name: assistant
role: system
body: "You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words."
variables:
company:
type: string
trusted: true
max_words:
type: integer
trusted: true

Step 1 — construct a Prompt (and validate)

Section titled “Step 1 — construct a Prompt (and validate)”

Read the definition file and hand its text to a from_* factory (Prompt::from_yaml / from_json / from_toml), or build from an already-built PromptDefinition value with Prompt::new. The library does no file I/O — the caller reads the file. All routes run the same validation immediately:

getting-started_rust_construct_from_text.rs
//! Construct a `Prompt` from a definition file with a `from_*` factory — validation runs
//! immediately. Standalone — `cargo run --example getting-started_rust_construct_from_text`.
use prompting_press::Prompt;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// The caller reads the definition; the library does no file I/O itself.
// Resolve the file next to this program (a real app uses its own path).
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?; // validates here, or Err
// The same definition in JSON or TOML parses into an identical Prompt:
// let assistant = Prompt::from_json(&fs::read_to_string(format!("{dir}/assistant.json"))?)?;
// let assistant = Prompt::from_toml(&fs::read_to_string(format!("{dir}/assistant.toml"))?)?;
assert_eq!(assistant.name(), "assistant");
assert_eq!(
assistant.body(),
"You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words."
);
Ok(())
}

Construction validates immediately. If the template referenced a variable that variables does not declare — say the body said {{ industry }} — construction returns Err(ConsumerError::Kernel(..)) before any render: a disagreement between the template and its declared variables is caught at construction, never as a silent empty render.

The render values are a caller-owned struct, validated by garde, whose field names match the prompt’s variables (company, max_words). This struct is the render-time gate: you hand it to render in Step 3, and its #[garde(..)] rules (here, max_words must be a positive integer) decide what data is allowed to reach the kernel. Declaring it is just the definition — the enforcement happens at render:

getting-started_rust_declare_vars.rs
//! Declare the typed Vars struct, validated by `garde`. Its field names must match the
//! prompt's `variables` (`company`, `max_words`); the `#[garde(..)]` rules run at render
//! time, before the kernel is touched. Standalone:
//! `cargo run --example getting-started_rust_declare_vars`.
use garde::Validate;
use serde::Serialize;
#[derive(Serialize, Validate)]
struct AssistantVars {
#[garde(length(min = 1))]
company: String,
#[garde(range(min = 1))]
max_words: i64,
}
fn main() {
// A valid instance passes garde validation; an out-of-range one fails.
let ok = AssistantVars {
company: "Acme Robotics".into(),
max_words: 50,
};
assert!(ok.validate().is_ok());
let bad = AssistantVars {
company: String::new(),
max_words: 0,
};
assert!(bad.validate().is_err());
}

Step 3 — render (the struct validates the data), and read the result

Section titled “Step 3 — render (the struct validates the data), and read the result”

Hand the Prompt from Step 1 the AssistantVars from Step 2. Validation runs first — valid data renders; invalid data (e.g. max_words: 0) returns Err(ConsumerError::Validation(..)) and the kernel is never touched. The sample shows both the passing render and the rejection:

getting-started_rust_render_and_read.rs
//! Render the `Prompt` with the typed `AssistantVars` and read the result fields.
//! Standalone — `cargo run --example getting-started_rust_render_and_read`.
use garde::Validate;
use prompting_press::{ConsumerError, GuardConfig, Prompt};
use serde::Serialize;
use std::fs;
#[derive(Serialize, Validate)]
struct AssistantVars {
#[garde(length(min = 1))]
company: String,
#[garde(range(min = 1))]
max_words: i64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
let vars = AssistantVars {
company: "Acme Robotics".into(),
max_words: 50,
};
let result = assistant.render(&vars, None, &GuardConfig::default(), false)?;
assert_eq!(
result.text,
"You are a support assistant for Acme Robotics. Keep your replies under 50 words."
); // the rendered body
assert_eq!(result.variant, "default"); // no variant selected → the default arm
// template_hash / render_hash are 64-char lowercase-hex SHA-256 strings.
assert_eq!(result.template_hash.len(), 64);
assert!(result
.template_hash
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
assert_eq!(result.render_hash.len(), 64);
assert!(result
.render_hash
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
// Typed Vars are validated at render, not just declared: `max_words: 0` violates
// `#[garde(range(min = 1))]`, so the kernel is never reached — render rejects it.
let bad_vars = AssistantVars {
company: "Acme Robotics".into(),
max_words: 0,
};
match assistant.render(&bad_vars, None, &GuardConfig::default(), false) {
Err(ConsumerError::Validation(rows)) => {
assert!(rows.iter().any(|r| r.field == "max_words"));
}
other => panic!("expected a validation error for max_words = 0, got {other:?}"),
}
Ok(())
}

render::<V>(&self, vars: &V, variant: Option<&str>, guard: &GuardConfig, reveal_render_detail: bool):

  • variant = None selects the default (root body) arm — the same arm assistant.body() showed in Step 1.
  • guard = &GuardConfig::default() opts out of the advisory guard text (so result.guard is None).
  • reveal_render_detail = false keeps render-error detail scrubbed; pass false in all production call sites.

Passing &vars here is what makes the typed-vars validation from Step 2 actually run — render calls vars.validate() before it ever touches the kernel. The generic V is resolved at the call site, so “this prompt has a validator for count” is a compile-time guarantee — no runtime validator object is stored on the Prompt. This is the Rust side of the per-language validator model.

getting-started_rust_complete_example.rs
//! The complete construct → declare → render walk in one program.
//! Standalone — `cargo run --example getting-started_rust_complete_example`.
use garde::Validate;
use prompting_press::GuardConfig;
use prompting_press::Prompt;
use serde::Serialize;
use std::fs;
#[derive(Serialize, Validate)]
struct AssistantVars {
#[garde(length(min = 1))]
company: String,
#[garde(range(min = 1))]
max_words: i64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Construct (validates here). The caller reads the definition; the library does
// no file I/O itself. Resolve the file next to this program (a real app uses its
// own path).
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
// 2 + 3. Render with typed, garde-validated vars.
let vars = AssistantVars {
company: "Acme Robotics".into(),
max_words: 50,
};
let result = assistant.render(&vars, None, &GuardConfig::default(), false)?;
println!("{}", result.text); // You are a support assistant for Acme Robotics. Keep your replies under 50 words.
println!("{}", result.template_hash); // 64-char hex
assert_eq!(
result.text,
"You are a support assistant for Acme Robotics. Keep your replies under 50 words."
);
assert_eq!(result.template_hash.len(), 64);
Ok(())
}
FieldValue
textThe rendered body text.
nameThe prompt name (def.name).
variantThe resolved variant name ("default" when none was selected).
template_hashSHA256(resolved variant template source) — lowercase hex.
render_hashSHA256(rendered output text) — lowercase hex.
guardOption<String> — advisory guard text when opted in; None otherwise.
getting-started_rust_error_types.rs
//! `ConsumerError` is a closed three-variant enum — the render match is exhaustive.
//! Standalone — `cargo run --example getting-started_rust_error_types`.
use garde::Validate;
use prompting_press::{error::code, ConsumerError, GuardConfig, Prompt};
use serde::Serialize;
use std::fs;
#[derive(Serialize, Validate)]
struct AssistantVars {
#[garde(length(min = 1))]
company: String,
#[garde(range(min = 1))]
max_words: i64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
// An empty company violates `#[garde(length(min = 1))]` → ConsumerError::Validation.
let vars = AssistantVars {
company: String::new(),
max_words: 50,
};
// ConsumerError is a closed three-variant enum — the match is exhaustive.
match assistant.render(&vars, None, &GuardConfig::default(), false) {
Ok(_result) => { /* ... */ }
Err(ConsumerError::Validation(rows)) => {
for row in &rows {
eprintln!("{}: {} [{}]", row.field, row.message, row.code);
}
// Every validation row carries the stable `"validation"` code.
assert!(rows.iter().all(|r| r.code == code::VALIDATION));
assert!(rows.iter().any(|r| r.field == "company"));
return Ok(());
}
Err(ConsumerError::Kernel(_rows)) => { /* parse/render/agreement failure */ }
Err(ConsumerError::Load(_msg)) => { /* malformed YAML/JSON/TOML */ }
}
Err("expected a validation error for the empty name".into())
}

Every FieldError row carries field, code (a stable string from error::code::*), and message. The closed code vocabulary: "validation", "unknown_variant", "undefined_variable", "parse", "render", "excluded_feature", "load".

docs current as of 0.2.0