use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GrammarFormat {
Lark,
Regex,
JsonSchema,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrammarConstraint {
pub format: GrammarFormat,
pub definition: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_property: Option<String>,
}
impl GrammarConstraint {
pub fn regex(pattern: impl Into<String>) -> Self {
Self {
format: GrammarFormat::Regex,
definition: pattern.into(),
input_property: None,
}
}
pub fn lark(grammar: impl Into<String>) -> Self {
Self {
format: GrammarFormat::Lark,
definition: grammar.into(),
input_property: None,
}
}
pub fn json_schema(schema: Value) -> Self {
Self {
format: GrammarFormat::JsonSchema,
definition: serde_json::to_string(&schema).unwrap_or_default(),
input_property: None,
}
}
pub fn with_input_property(mut self, property: impl Into<String>) -> Self {
self.input_property = Some(property.into());
self
}
}
pub trait ConstrainedSamplingSupport {
fn supports_grammar_format(&self, format: &GrammarFormat) -> bool;
fn constraint_to_params(&self, constraint: &GrammarConstraint) -> Option<Value>;
}
pub fn validate_regex(pattern: &str) -> Result<(), String> {
let mut bracket_depth = 0;
let mut paren_depth = 0;
let mut escape_next = false;
for ch in pattern.chars() {
if escape_next {
escape_next = false;
continue;
}
match ch {
'\\' => escape_next = true,
'[' => bracket_depth += 1,
']' => {
if bracket_depth == 0 {
return Err("Unbalanced ] in regex".to_string());
}
bracket_depth -= 1;
}
'(' => paren_depth += 1,
')' => {
if paren_depth == 0 {
return Err("Unbalanced ) in regex".to_string());
}
paren_depth -= 1;
}
_ => {}
}
}
if escape_next {
return Err("Trailing backslash in regex".to_string());
}
if bracket_depth != 0 {
return Err("Unbalanced [ in regex".to_string());
}
if paren_depth != 0 {
return Err("Unbalanced ( in regex".to_string());
}
Ok(())
}
pub fn validate_lark_grammar(grammar: &str) -> Result<(), String> {
if grammar.trim().is_empty() {
return Err("Grammar is empty".to_string());
}
let mut brace_count = 0;
for ch in grammar.chars() {
match ch {
'{' => brace_count += 1,
'}' => {
brace_count -= 1;
if brace_count < 0 {
return Err("Unbalanced braces in grammar".to_string());
}
}
_ => {}
}
}
if brace_count != 0 {
return Err("Unbalanced braces in grammar".to_string());
}
if !grammar.contains(':') {
return Err("Grammar must contain at least one rule (e.g., 'start: ...')".to_string());
}
Ok(())
}
pub fn validate_json_schema(schema: &Value) -> Result<(), String> {
if !schema.is_object() {
return Err("JSON Schema must be an object".to_string());
}
let obj = schema.as_object().unwrap();
if !obj.contains_key("type") {
return Err("JSON Schema must have a 'type' field".to_string());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_regex_valid() {
assert!(validate_regex(r"^\d{3}-\d{2}-\d{4}$").is_ok());
assert!(validate_regex(r"[a-z]+").is_ok());
assert!(validate_regex(r"").is_ok()); }
#[test]
fn test_validate_regex_invalid() {
assert!(validate_regex(r"[unclosed").is_err());
assert!(validate_regex(r"(?P<invalid").is_err());
}
#[test]
fn test_validate_lark_grammar_valid() {
let grammar = r#"
start: WORD ("," WORD)*
WORD: /\w+/
"#;
assert!(validate_lark_grammar(grammar).is_ok());
}
#[test]
fn test_validate_lark_grammar_empty() {
assert!(validate_lark_grammar("").is_err());
assert!(validate_lark_grammar(" ").is_err());
}
#[test]
fn test_validate_lark_grammar_unbalanced_braces() {
assert!(validate_lark_grammar("start: {unclosed").is_err());
assert!(validate_lark_grammar("start: }extra{").is_err());
}
#[test]
fn test_validate_lark_grammar_no_rules() {
assert!(validate_lark_grammar("no colons here").is_err());
}
#[test]
fn test_validate_json_schema_valid() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
assert!(validate_json_schema(&schema).is_ok());
}
#[test]
fn test_validate_json_schema_not_object() {
let schema = serde_json::json!("string");
assert!(validate_json_schema(&schema).is_err());
}
#[test]
fn test_validate_json_schema_missing_type() {
let schema = serde_json::json!({
"properties": {
"name": {"type": "string"}
}
});
assert!(validate_json_schema(&schema).is_err());
}
#[test]
fn test_grammar_constraint_regex() {
let constraint = GrammarConstraint::regex(r"^\d{3}$");
assert_eq!(constraint.format, GrammarFormat::Regex);
assert_eq!(constraint.definition, r"^\d{3}$");
assert!(constraint.input_property.is_none());
}
#[test]
fn test_grammar_constraint_with_property() {
let constraint = GrammarConstraint::regex(r"\d+").with_input_property("value");
assert_eq!(constraint.input_property, Some("value".to_string()));
}
}