use crate::structured::error::{ValidationError, ValidationResult};
use crate::structured::validator::OutputValidator;
use regex::Regex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum JsonMode {
Json,
JsonSchema,
Off,
}
impl JsonMode {
pub fn as_str(&self) -> &'static str {
match self {
JsonMode::Json => "json_object",
JsonMode::JsonSchema => "json_schema",
JsonMode::Off => "",
}
}
}
impl std::fmt::Display for JsonMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for JsonMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"json_object" => Ok(JsonMode::Json),
"json_schema" => Ok(JsonMode::JsonSchema),
"off" | "" => Ok(JsonMode::Off),
_ => Err(format!("Unknown JSON mode: {}", s)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonModeConfig {
pub mode: JsonMode,
pub schema: Option<serde_json::Value>,
pub schema_name: String,
pub strict: bool,
}
impl JsonModeConfig {
pub fn json_object() -> Self {
Self {
mode: JsonMode::Json,
schema: None,
schema_name: "response".to_string(),
strict: true,
}
}
pub fn from_schema(schema: serde_json::Value, name: impl Into<String>, strict: bool) -> Self {
Self {
mode: JsonMode::JsonSchema,
schema: Some(schema),
schema_name: name.into(),
strict,
}
}
pub fn to_openai_format(&self) -> serde_json::Value {
match self.mode {
JsonMode::Off => serde_json::json!({}),
JsonMode::Json => serde_json::json!({
"response_format": {
"type": self.mode.as_str()
}
}),
JsonMode::JsonSchema => {
let schema = self
.schema
.as_ref()
.expect("Schema required for JsonSchema mode");
serde_json::json!({
"response_format": {
"type": self.mode.as_str(),
"json_schema": {
"name": self.schema_name,
"strict": self.strict,
"schema": schema
}
}
})
}
}
}
pub fn to_anthropic_format(&self) -> serde_json::Value {
serde_json::json!({})
}
}
#[derive(Debug, Clone)]
pub struct StructuredOutput {
pub raw: String,
pub parsed: Option<serde_json::Value>,
pub validation_result: ValidationResult,
}
impl StructuredOutput {
pub fn from_response_unvalidated(content: impl Into<String>) -> Self {
let content = content.into();
let content_str = content.trim();
let parsed = Self::parse_json(content_str);
let validation_result = ValidationResult::success(
parsed
.clone()
.unwrap_or_else(|| serde_json::Value::String(content_str.to_string())),
);
Self {
raw: content,
parsed,
validation_result,
}
}
pub fn validate(&mut self, validator: &OutputValidator) {
if let Some(parsed) = &self.parsed {
self.validation_result = validator.validate(parsed);
}
}
pub fn from_response(content: impl Into<String>, validator: &OutputValidator) -> Self {
let mut output = Self::from_response_unvalidated(content);
output.validate(validator);
output
}
fn parse_json(text: &str) -> Option<serde_json::Value> {
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(text) {
return Some(parsed);
}
let patterns = [
r"```json\s*([\s\S]*?)\s*```",
r"```\s*([\s\S]*?)\s*```",
r"\{[\s\S]*\}",
r"\[[\s\S]*\]",
];
for pattern in patterns {
if let Ok(re) = Regex::new(pattern) {
if let Some(captures) = re.captures(text) {
let candidate = match captures.get(1) {
Some(inner) => inner.as_str(),
None => captures.get(0).map(|c| c.as_str()).unwrap_or(text),
};
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(candidate.trim())
{
return Some(parsed);
}
}
}
}
None
}
pub fn is_valid(&self) -> bool {
self.validation_result.is_valid()
}
pub fn data(&self) -> serde_json::Value {
if let Some(data) = self.validation_result.data() {
return data.clone();
}
if let Some(parsed) = &self.parsed {
return parsed.clone();
}
serde_json::Value::String(self.raw.clone())
}
pub fn raw(&self) -> &str {
&self.raw
}
pub fn parsed(&self) -> Option<&serde_json::Value> {
self.parsed.as_ref()
}
pub fn validation_result(&self) -> &ValidationResult {
&self.validation_result
}
pub fn errors(&self) -> Vec<ValidationError> {
if self.validation_result.is_valid() {
Vec::new()
} else {
self.validation_result.errors.clone()
}
}
pub fn error_messages(&self) -> Vec<String> {
self.validation_result.error_messages()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_mode_config_json_object() {
let config = JsonModeConfig::json_object();
assert_eq!(config.mode, JsonMode::Json);
assert!(config.schema.is_none());
assert_eq!(config.schema_name, "response");
assert!(config.strict);
}
#[test]
fn test_json_mode_config_from_schema() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"}
}
});
let config = JsonModeConfig::from_schema(schema, "User", true);
assert_eq!(config.mode, JsonMode::JsonSchema);
assert!(config.schema.is_some());
assert_eq!(config.schema_name, "User");
assert!(config.strict);
}
#[test]
fn test_json_mode_config_to_openai_format_json() {
let config = JsonModeConfig::json_object();
let openai = config.to_openai_format();
assert_eq!(openai["response_format"]["type"], "json_object");
}
#[test]
fn test_json_mode_config_to_openai_format_json_schema() {
let schema = serde_json::json!({
"type": "string"
});
let config = JsonModeConfig::from_schema(schema.clone(), "test", false);
let openai = config.to_openai_format();
assert_eq!(openai["response_format"]["type"], "json_schema");
assert_eq!(openai["response_format"]["json_schema"]["name"], "test");
assert_eq!(openai["response_format"]["json_schema"]["strict"], false);
assert_eq!(openai["response_format"]["json_schema"]["schema"], schema);
}
#[test]
fn test_json_mode_config_to_openai_format_off() {
let config = JsonModeConfig {
mode: JsonMode::Off,
schema: None,
schema_name: "test".to_string(),
strict: false,
};
let openai = config.to_openai_format();
assert_eq!(openai, serde_json::json!({}));
}
#[test]
fn test_json_mode_config_to_anthropic_format() {
let config = JsonModeConfig::json_object();
let anthropic = config.to_anthropic_format();
assert_eq!(anthropic, serde_json::json!({}));
}
#[test]
fn test_structured_output_valid_json() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"result": {"type": "string"}
}
});
let validator = OutputValidator::lenient(schema);
let output = StructuredOutput::from_response(r#"{"result": "success"}"#, &validator);
assert!(output.is_valid());
assert!(output.parsed().is_some());
}
#[test]
fn test_structured_output_invalid_json() {
let output = StructuredOutput::from_response_unvalidated("not json");
assert!(output.is_valid());
assert!(output.parsed().is_none());
}
#[test]
fn test_structured_output_parsed_json() {
let output = StructuredOutput::from_response_unvalidated(r#"{"valid": true}"#);
assert_eq!(output.parsed().unwrap()["valid"], true);
}
#[test]
fn test_structured_output_json_from_markdown() {
let output = StructuredOutput::from_response_unvalidated(
r#"Here is the JSON:
```json
{"result": "success"}
```"#,
);
assert_eq!(output.parsed().unwrap()["result"], "success");
}
#[test]
fn test_structured_output_data_priority() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"value": {"type": "string"}
}
});
let validator = OutputValidator::lenient(schema);
let mut output = StructuredOutput::from_response_unvalidated(r#"{"value": "test"}"#);
output.validate(&validator);
let data = output.data();
assert_eq!(*output.validation_result.data().unwrap(), data);
}
#[test]
fn test_structured_output_validate_method() {
let schema = serde_json::json!({"type": "integer"});
let validator = OutputValidator::lenient(schema);
let mut output = StructuredOutput::from_response_unvalidated(r#"{"value": "test"}"#);
output.validate(&validator);
assert!(!output.is_valid());
assert!(!output.errors().is_empty());
}
#[test]
fn test_structured_output_errors() {
let schema = serde_json::json!({"type": "integer"});
let validator = OutputValidator::lenient(schema);
let mut output = StructuredOutput::from_response_unvalidated(r#"{"value": "not integer"}"#);
output.validate(&validator);
assert!(!output.is_valid());
assert!(!output.errors().is_empty());
}
}