Skip to content

Commit bfa98e8

Browse files
Render none as null (#2037)
Fix #2008 <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Render `none` as `null` in Jinja templates, handling both top-level and nested cases, with new tests added for verification. > > - **Behavior**: > - Render `none` as `null` in `jinja_helpers.rs` using a custom formatter for top-level values. > - Handle nested `none` values in `baml_value_to_jinja_value.rs` by converting them to `BamlNull`. > - **Classes and Structs**: > - Add `MinijinjaBamlList` and `BamlNull` structs in `baml_value_to_jinja_value.rs` to support list and null rendering. > - **Tests**: > - Add tests in `lib.rs` to verify rendering of `none` as `null` in various scenarios, including nested structures and lists. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=BoundaryML%2Fbaml&utm_source=github&utm_medium=referral)<sup> for 309a64a. You can [customize](https://app.ellipsis.dev/BoundaryML/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> <!-- ELLIPSIS_HIDDEN -->
1 parent 8d04233 commit bfa98e8

5 files changed

Lines changed: 422 additions & 115 deletions

File tree

‎engine/baml-lib/baml-core/src/ir/jinja_helpers.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@ use regex::Regex;
66

77
pub fn get_env<'a>() -> minijinja::Environment<'a> {
88
let mut env = minijinja::Environment::new();
9+
10+
env.set_formatter(|output, state, value| {
11+
// Top level (non-nested) none value is handled here.
12+
// Nested none values are handled in std::fmt::Display impl for
13+
// MinijinjaBamlClass and MinijinjaBamlList.
14+
// File is jinja-runtime/src/baml_value_to_jinja_value.rs.
15+
//
16+
// This is a little confusing and would be nice to replace all nones
17+
// with nulls in a single place but had no luck getting it to work,
18+
// this commit has commented code that attempts to do so:
19+
// https://github.com/BoundaryML/baml/pull/2037/commits/6facd2805f79be3e2000dc61058e772d9c5943be
20+
let value = if value.is_none() {
21+
&Value::from("null")
22+
} else {
23+
value
24+
};
25+
26+
minijinja::escape_formatter(output, state, value)
27+
});
28+
929
env.set_debug(true);
1030
env.set_trim_blocks(true);
1131
env.set_lstrip_blocks(true);

‎engine/baml-lib/jinja-runtime/src/baml_value_to_jinja_value.rs‎

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use baml_types::EvaluationContext;
44
use indexmap::IndexMap;
55
use internal_baml_core::ir::repr::IntermediateRepr;
66
use internal_baml_core::ir::IRHelper;
7-
use minijinja::value::{Object, ObjectRepr, Enumerator};
7+
use minijinja::value::{Enumerator, Object, ObjectRepr};
88

99
use crate::{BamlMedia, BamlValue};
1010

@@ -38,7 +38,7 @@ impl IntoMiniJinjaValue for BamlValue {
3838
.iter()
3939
.map(|v| v.to_minijinja_value(ir, eval_ctx))
4040
.collect();
41-
minijinja::Value::from(list)
41+
minijinja::Value::from_object(MinijinjaBamlList { list })
4242
}
4343
BamlValue::Media(i) => i.to_minijinja_value(ir, eval_ctx),
4444
// For enums and classes we compute the aliases from the IR, and generate custom jinja structs that print out the alias if stringified.
@@ -199,7 +199,16 @@ impl std::fmt::Display for MinijinjaBamlClass {
199199
// replace the keys with the aliases
200200
for (k, v) in self.class.iter() {
201201
let alias = self.key_to_alias.get(k).unwrap_or(k);
202-
map.insert(alias.to_string(), v.clone());
202+
203+
// This handles nested none values.
204+
// Top level none values are handled in jinja-runtime/src/jinja_helpers.rs.
205+
let value = if v.is_none() {
206+
minijinja::Value::from_object(BamlNull)
207+
} else {
208+
v.clone()
209+
};
210+
211+
map.insert(alias.to_string(), value);
203212
}
204213
// Use pretty-printed JSON formatting as expected by tests
205214
write!(f, "{:#?}", map)
@@ -223,11 +232,97 @@ impl Object for MinijinjaBamlClass {
223232
}
224233

225234
fn enumerate(self: &Arc<Self>) -> Enumerator {
226-
let keys: Vec<minijinja::Value> = self.class.keys().map(|k| minijinja::Value::from(k.as_str())).collect();
235+
let keys: Vec<minijinja::Value> = self
236+
.class
237+
.keys()
238+
.map(|k| minijinja::Value::from(k.as_str()))
239+
.collect();
227240
Enumerator::Values(keys)
228241
}
229242

230243
fn render(self: &Arc<Self>, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231244
std::fmt::Display::fmt(self, f)
232245
}
233246
}
247+
248+
// List
249+
250+
struct MinijinjaBamlList {
251+
list: Vec<minijinja::Value>,
252+
}
253+
254+
impl std::fmt::Display for MinijinjaBamlList {
255+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
256+
let mut list = f.debug_list();
257+
258+
// Replace "none" with "null". See comments in std::fmt::Display impl
259+
// for MinijinjaBamlClass and see BamlNull.
260+
for value in &self.list {
261+
if value.is_none() {
262+
list.entry(&minijinja::Value::from_object(BamlNull));
263+
} else {
264+
list.entry(value);
265+
}
266+
}
267+
268+
list.finish()
269+
}
270+
}
271+
272+
impl std::fmt::Debug for MinijinjaBamlList {
273+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
274+
std::fmt::Display::fmt(self, f)
275+
}
276+
}
277+
278+
impl Object for MinijinjaBamlList {
279+
fn repr(self: &Arc<Self>) -> ObjectRepr {
280+
ObjectRepr::Seq
281+
}
282+
283+
fn get_value(self: &Arc<Self>, key: &minijinja::Value) -> Option<minijinja::Value> {
284+
self.list.get(key.as_usize()?).cloned()
285+
}
286+
287+
fn enumerate(self: &Arc<Self>) -> Enumerator {
288+
Enumerator::Seq(self.list.len())
289+
}
290+
291+
fn enumerator_len(self: &Arc<Self>) -> Option<usize> {
292+
Some(self.list.len())
293+
}
294+
295+
fn render(self: &Arc<Self>, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296+
std::fmt::Display::fmt(self, f)
297+
}
298+
}
299+
300+
// Null
301+
302+
/// This only exists because [`minijinja`] renders "none" instead of "null".
303+
///
304+
/// Don't use for anything else other than rendering because if we use this
305+
/// instead of `minijinja::Value::from(())` then the `{% if v is none %}`
306+
/// comparison in Jinja will not work.
307+
#[derive(Debug)]
308+
pub(crate) struct BamlNull;
309+
310+
impl std::fmt::Display for BamlNull {
311+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312+
f.write_str("null")
313+
}
314+
}
315+
316+
impl Object for BamlNull {
317+
fn repr(self: &Arc<Self>) -> ObjectRepr {
318+
ObjectRepr::Plain
319+
}
320+
321+
fn is_true(self: &Arc<Self>) -> bool {
322+
false
323+
}
324+
325+
fn render(self: &Arc<Self>, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326+
f.write_str("null")
327+
}
328+
}

0 commit comments

Comments
 (0)