use std::sync::Arc;
use axum::http::StatusCode;
use axum::Json;
use ferrox_models::grammar::Grammar;
use crate::ApiError;
const ROOT: &str = "root";
pub(crate) fn compile(src: &str) -> Result<Arc<Grammar>, ApiError> {
Grammar::from_str_with_root(src, ROOT)
.map(Arc::new)
.map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": {
"message": format!("grammar: {e}"),
"type": "invalid_request_error",
"param": "grammar",
}
})),
)
})
}
pub(crate) fn for_request(
grammar: Option<&str>,
response_format: Option<&serde_json::Value>,
) -> Result<Option<Arc<Grammar>>, ApiError> {
if let Some(kind) = response_format
.and_then(|v| v.get("type"))
.and_then(|v| v.as_str())
{
if kind == "json_schema" {
return Err(json_schema_not_implemented());
}
}
match grammar.map(str::trim).filter(|s| !s.is_empty()) {
Some(src) => compile(src).map(Some),
None => Ok(None),
}
}
fn json_schema_not_implemented() -> ApiError {
(
StatusCode::NOT_IMPLEMENTED,
Json(serde_json::json!({
"error": {
"message":
"response_format json_schema is not implemented: the grammar engine and the \
sampler hook are wired, but converting a JSON schema to a GBNF grammar is \
not. Send the equivalent grammar in the \"grammar\" field, or use \
response_format json_object for the best-effort character-class mode.",
"type": "invalid_request_error",
"param": "response_format",
}
})),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_gbnf_grammar_compiles_and_starts_at_root() {
let g = for_request(Some(r#"root ::= "a"+"#), None)
.expect("a valid grammar is not an error")
.expect("a grammar was asked for");
assert!(!g.stacks().is_empty(), "the machine has a live stack");
}
#[test]
fn no_grammar_field_means_no_grammar() {
assert!(for_request(None, None).unwrap().is_none());
assert!(for_request(Some(" "), None).unwrap().is_none());
}
#[test]
fn an_unparseable_grammar_is_a_400_naming_the_field() {
let (status, Json(body)) =
for_request(Some(r#"root ::= "a"#), None).expect_err("this does not parse");
assert_eq!(status, StatusCode::BAD_REQUEST);
assert_eq!(body["error"]["param"], "grammar");
}
#[test]
fn a_grammar_with_no_root_rule_is_refused() {
let (status, _) =
for_request(Some(r#"start ::= "a""#), None).expect_err("there is no root rule");
assert_eq!(status, StatusCode::BAD_REQUEST);
}
#[test]
fn json_schema_is_refused_by_name_rather_than_ignored() {
let fmt = serde_json::json!({"type": "json_schema", "json_schema": {"schema": {}}});
let (status, Json(body)) = for_request(None, Some(&fmt)).expect_err("not implemented yet");
assert_eq!(status, StatusCode::NOT_IMPLEMENTED);
let message = body["error"]["message"].as_str().unwrap();
assert!(message.contains("JSON schema"), "{message}");
let (status, _) = for_request(Some(r#"root ::= "a""#), Some(&fmt))
.expect_err("two constraints, one of which cannot be honoured");
assert_eq!(status, StatusCode::NOT_IMPLEMENTED);
}
#[test]
fn json_object_is_not_the_schema_arm() {
let fmt = serde_json::json!({"type": "json_object"});
assert!(for_request(None, Some(&fmt)).unwrap().is_none());
}
}