use std::collections::HashSet;
use std::fmt;
use std::sync::OnceLock;
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::card::prompt::PromptSpec;
use crate::card::prompt::validate::PromptError;
use crate::error::WyrdError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(transparent)]
pub struct ParameterName(String);
impl ParameterName {
pub fn new(value: impl Into<String>) -> Result<Self, WyrdError> {
let value = value.into();
if !is_valid_parameter_name(&value) {
return Err(PromptError::InvalidVariableName { name: value }.into());
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ParameterName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
pub fn is_valid_parameter_name(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
pub fn text_placeholder_regex() -> &'static Regex {
static PLACEHOLDER_RE: OnceLock<Regex> = OnceLock::new();
PLACEHOLDER_RE.get_or_init(|| {
match Regex::new(r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}|\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}") {
Ok(regex) => regex,
Err(error) => panic!("PromptCard placeholder regex is static and valid: {error}"),
}
})
}
pub fn media_placeholder_regex() -> &'static Regex {
static PLACEHOLDER_RE: OnceLock<Regex> = OnceLock::new();
PLACEHOLDER_RE.get_or_init(
|| match Regex::new(r"\$\{media:([a-zA-Z_][a-zA-Z0-9_]*)\}") {
Ok(regex) => regex,
Err(error) => panic!("PromptCard media placeholder regex is static and valid: {error}"),
},
)
}
pub fn extract_text_placeholders(spec: &PromptSpec) -> Result<Vec<String>, WyrdError> {
extract_with_regex(spec, text_placeholder_regex())
}
pub fn extract_media_placeholders(spec: &PromptSpec) -> Result<Vec<String>, WyrdError> {
extract_with_regex(spec, media_placeholder_regex())
}
fn extract_with_regex(spec: &PromptSpec, regex: &Regex) -> Result<Vec<String>, WyrdError> {
let json = serde_json::to_string(&spec.prompt.request).map_err(|error| {
WyrdError::from(PromptError::SerializeRequest {
message: error.to_string(),
})
})?;
let mut out = Vec::new();
let mut seen = HashSet::new();
for capture in regex.captures_iter(&json) {
let Some(name_match) = capture.get(1).or_else(|| capture.get(2)) else {
continue;
};
let name = name_match.as_str().to_owned();
if seen.insert(name.clone()) {
out.push(name);
}
}
Ok(out)
}