Getting started — Rust
Install
Section titled “Install”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.
Write a prompt definition
Section titled “Write a prompt definition”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:
name: assistantrole: systembody: "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{ "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 } }}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
[variables.max_words]type = "integer"trusted = trueConstruct and render
Section titled “Construct and render”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:
//! 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(())}//! Construct a `Prompt` from an already-built `PromptDefinition` value with `Prompt::new`//! — same validation as the `from_*` text factories. Standalone://! `cargo run --example getting-started_rust_construct_from_definition`.
use prompting_press::{Prompt, PromptDefinition};
fn main() -> Result<(), Box<dyn std::error::Error>> { // PromptDefinition is the codegen'd shape. Building it inline is verbose (its // fields are newtypes), so it's usually deserialized — here, from a JSON literal: let def: PromptDefinition = serde_json::from_value(serde_json::json!({ "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 } } }))?;
let assistant = Prompt::new(def)?; // same validation as the from_* factories
assert_eq!(assistant.name(), "assistant"); Ok(())}The from_* text factories cover the common case; Prompt::new is the path for an
already-constructed PromptDefinition (e.g. one built programmatically).
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.
Step 2 — declare the typed Vars
Section titled “Step 2 — declare the typed Vars”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:
//! 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:
//! 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 = Noneselects the default (root body) arm — the same armassistant.body()showed in Step 1.guard = &GuardConfig::default()opts out of the advisory guard text (soresult.guardisNone).reveal_render_detail = falsekeeps render-error detail scrubbed; passfalsein 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.
Complete example
Section titled “Complete example”//! 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(())}What RenderResult carries
Section titled “What RenderResult carries”| Field | Value |
|---|---|
text | The rendered body text. |
name | The prompt name (def.name). |
variant | The resolved variant name ("default" when none was selected). |
template_hash | SHA256(resolved variant template source) — lowercase hex. |
render_hash | SHA256(rendered output text) — lowercase hex. |
guard | Option<String> — advisory guard text when opted in; None otherwise. |
Error types
Section titled “Error types”//! `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".
Next steps
Section titled “Next steps”- Variants — declare alternative bodies and select one at render.
- Deriving a prompt — produce a changed copy of an immutable prompt.
- Metadata — attach and read back opaque prompt-level and per-variant metadata.
- Compose multi-message prompts — aggregate
Promptobjects into a role-tagged sequence. - The advisory guard — tag untrusted variables and surface the advisory guard text.
- Lint prompts in CI — wire
prompt.check()as a test that fails the build. - Template features — which MiniJinja features are supported (and which are deliberately excluded).
- Rust API reference — full method signatures.
docs current as of 0.2.0