use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PromptError {
#[error("Template not found: {0}")]
TemplateNotFound(String),
#[error("Required variable not provided: {0}")]
MissingVariable(String),
#[error("Variable type mismatch for '{name}': expected {expected}, got {actual}")]
TypeMismatch {
name: String,
expected: String,
actual: String,
},
#[error("Validation failed for variable '{name}': {reason}")]
ValidationFailed { name: String, reason: String },
#[error("Parse error: {0}")]
ParseError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("YAML error: {0}")]
YamlError(String),
}
pub type PromptResult<T> = Result<T, PromptError>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum VariableType {
#[default]
String,
Integer,
Float,
Boolean,
List,
Json,
}
impl VariableType {
pub fn validate(&self, value: &str) -> bool {
match self {
VariableType::String => true,
VariableType::Integer => value.parse::<i64>().is_ok(),
VariableType::Float => value.parse::<f64>().is_ok(),
VariableType::Boolean => {
matches!(value.to_lowercase().as_str(), "true" | "false" | "1" | "0")
}
VariableType::List => value.starts_with('[') && value.ends_with(']'),
VariableType::Json => serde_json::from_str::<serde_json::Value>(value).is_ok(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptVariable {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub var_type: VariableType,
#[serde(default = "default_true")]
pub required: bool,
#[serde(default)]
pub default: Option<String>,
#[serde(default)]
pub pattern: Option<String>,
#[serde(default)]
pub enum_values: Option<Vec<String>>,
}
fn default_true() -> bool {
true
}
impl PromptVariable {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
var_type: VariableType::String,
required: true,
default: None,
pattern: None,
enum_values: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_type(mut self, var_type: VariableType) -> Self {
self.var_type = var_type;
self
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn with_default(mut self, default: impl Into<String>) -> Self {
self.default = Some(default.into());
self.required = false;
self
}
pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
self.pattern = Some(pattern.into());
self
}
pub fn with_enum(mut self, values: Vec<String>) -> Self {
self.enum_values = Some(values);
self
}
pub fn validate(&self, value: &str) -> PromptResult<()> {
if !self.var_type.validate(value) {
return Err(PromptError::TypeMismatch {
name: self.name.clone(),
expected: format!("{:?}", self.var_type),
actual: "invalid".to_string(),
});
}
if let Some(ref pattern) = self.pattern {
let re =
regex::Regex::new(pattern).map_err(|e| PromptError::ParseError(e.to_string()))?;
if !re.is_match(value) {
return Err(PromptError::ValidationFailed {
name: self.name.clone(),
reason: format!("Value does not match pattern: {}", pattern),
});
}
}
if let Some(ref enum_values) = self.enum_values
&& !enum_values.contains(&value.to_string())
{
return Err(PromptError::ValidationFailed {
name: self.name.clone(),
reason: format!("Value must be one of: {:?}", enum_values),
});
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptTemplate {
pub id: String,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub content: String,
#[serde(default)]
pub variables: Vec<PromptVariable>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
impl PromptTemplate {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
name: None,
description: None,
content: String::new(),
variables: Vec::new(),
tags: Vec::new(),
version: None,
metadata: HashMap::new(),
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_content(mut self, content: impl Into<String>) -> Self {
self.content = content.into();
self.parse_variables();
self
}
pub fn with_variable(mut self, variable: PromptVariable) -> Self {
self.variables.push(variable);
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
fn parse_variables(&mut self) {
}
pub fn variable_names(&self) -> Vec<&str> {
self.variables.iter().map(|v| v.name.as_str()).collect()
}
pub fn extract_variables(&self) -> Vec<String> {
let re = regex::Regex::new(r"\{(\w+)\}").unwrap();
let mut vars = std::collections::HashSet::new();
for cap in re.captures_iter(&self.content) {
vars.insert(cap[1].to_string());
}
vars.into_iter().collect()
}
pub fn required_variables(&self) -> Vec<&PromptVariable> {
self.variables.iter().filter(|v| v.required).collect()
}
pub fn render(&self, vars: &[(&str, &str)]) -> PromptResult<String> {
let var_map: HashMap<&str, &str> = vars.iter().copied().collect();
self.render_with_map(&var_map)
}
pub fn render_with_map(&self, vars: &HashMap<&str, &str>) -> PromptResult<String> {
let mut result = self.content.clone();
for var_def in &self.variables {
let placeholder = format!("{{{}}}", var_def.name);
if let Some(&value) = vars.get(var_def.name.as_str()) {
var_def.validate(value)?;
result = result.replace(&placeholder, value);
} else if let Some(ref default) = var_def.default {
result = result.replace(&placeholder, default);
} else if var_def.required {
return Err(PromptError::MissingVariable(var_def.name.clone()));
}
}
let re = regex::Regex::new(r"\{(\w+)\}").unwrap();
let defined_vars: std::collections::HashSet<_> =
self.variables.iter().map(|v| v.name.as_str()).collect();
let mut missing = Vec::new();
for cap in re.captures_iter(&result.clone()) {
let var_name = &cap[1];
if !defined_vars.contains(var_name) {
if let Some(&value) = vars.get(var_name) {
let placeholder = format!("{{{}}}", var_name);
result = result.replace(&placeholder, value);
} else {
missing.push(var_name.to_string());
}
}
}
if !missing.is_empty() {
return Err(PromptError::MissingVariable(missing.join(", ")));
}
Ok(result)
}
pub fn render_with_owned_map(&self, vars: &HashMap<String, String>) -> PromptResult<String> {
let borrowed: HashMap<&str, &str> =
vars.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
self.render_with_map(&borrowed)
}
pub fn partial_render(&self, vars: &[(&str, &str)]) -> String {
let var_map: HashMap<&str, &str> = vars.iter().copied().collect();
let mut result = self.content.clone();
for (name, value) in var_map {
let placeholder = format!("{{{}}}", name);
result = result.replace(&placeholder, value);
}
result
}
pub fn is_valid_with(&self, vars: &[&str]) -> bool {
let var_set: std::collections::HashSet<_> = vars.iter().copied().collect();
for var_def in &self.variables {
if var_def.required
&& var_def.default.is_none()
&& !var_set.contains(var_def.name.as_str())
{
return false;
}
}
let re = regex::Regex::new(r"\{(\w+)\}").unwrap();
let defined_vars: std::collections::HashSet<_> =
self.variables.iter().map(|v| v.name.as_str()).collect();
for cap in re.captures_iter(&self.content) {
let var_name = &cap[1];
if !defined_vars.contains(var_name) && !var_set.contains(var_name) {
return false;
}
}
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptComposition {
pub id: String,
#[serde(default)]
pub description: Option<String>,
pub template_ids: Vec<String>,
#[serde(default = "default_separator")]
pub separator: String,
}
fn default_separator() -> String {
"\n\n".to_string()
}
impl PromptComposition {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
description: None,
template_ids: Vec::new(),
separator: "\n\n".to_string(),
}
}
pub fn add_template(mut self, template_id: impl Into<String>) -> Self {
self.template_ids.push(template_id.into());
self
}
pub fn with_separator(mut self, sep: impl Into<String>) -> Self {
self.separator = sep.into();
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_template_basic() {
let template = PromptTemplate::new("test")
.with_content("Hello, {name}!")
.with_description("A greeting template");
assert_eq!(template.id, "test");
assert_eq!(template.extract_variables(), vec!["name"]);
let result = template.render(&[("name", "World")]).unwrap();
assert_eq!(result, "Hello, World!");
}
#[test]
fn test_template_multiple_vars() {
let template = PromptTemplate::new("test")
.with_content("Hello, {name}! Welcome to {place}. Your role is {role}.");
let result = template
.render(&[
("name", "Alice"),
("place", "Wonderland"),
("role", "explorer"),
])
.unwrap();
assert_eq!(
result,
"Hello, Alice! Welcome to Wonderland. Your role is explorer."
);
}
#[test]
fn test_template_with_default() {
let template = PromptTemplate::new("test")
.with_content("Hello, {name}!")
.with_variable(PromptVariable::new("name").with_default("World"));
let result = template.render(&[]).unwrap();
assert_eq!(result, "Hello, World!");
let result = template.render(&[("name", "Alice")]).unwrap();
assert_eq!(result, "Hello, Alice!");
}
#[test]
fn test_template_missing_required() {
let template = PromptTemplate::new("test").with_content("Hello, {name}!");
let result = template.render(&[]);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
PromptError::MissingVariable(_)
));
}
#[test]
fn test_variable_type_validation() {
assert!(VariableType::String.validate("anything"));
assert!(VariableType::Integer.validate("123"));
assert!(!VariableType::Integer.validate("abc"));
assert!(VariableType::Float.validate("3.14"));
assert!(VariableType::Boolean.validate("true"));
assert!(VariableType::Boolean.validate("false"));
assert!(VariableType::Json.validate(r#"{"key": "value"}"#));
}
#[test]
fn test_variable_enum() {
let var = PromptVariable::new("language")
.with_enum(vec!["rust".to_string(), "python".to_string()]);
assert!(var.validate("rust").is_ok());
assert!(var.validate("python").is_ok());
assert!(var.validate("java").is_err());
}
#[test]
fn test_partial_render() {
let template =
PromptTemplate::new("test").with_content("Hello, {name}! Your {item} is ready.");
let result = template.partial_render(&[("name", "Alice")]);
assert_eq!(result, "Hello, Alice! Your {item} is ready.");
}
#[test]
fn test_is_valid_with() {
let template = PromptTemplate::new("test")
.with_content("{required_var} and {optional_var}")
.with_variable(PromptVariable::new("required_var"))
.with_variable(PromptVariable::new("optional_var").with_default("default"));
assert!(template.is_valid_with(&["required_var"]));
assert!(!template.is_valid_with(&[]));
assert!(!template.is_valid_with(&["optional_var"]));
}
}