#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct JsonParseOptions {
pub ignore_unknown_enum_values: bool,
pub strict_extension_keys: bool,
}
impl JsonParseOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn ignore_unknown_enum_values(mut self, ignore: bool) -> Self {
self.ignore_unknown_enum_values = ignore;
self
}
#[must_use]
pub fn strict_extension_keys(mut self, strict: bool) -> Self {
self.strict_extension_keys = strict;
self
}
}
#[cfg(feature = "std")]
mod std_impl {
use super::JsonParseOptions;
use std::cell::Cell;
thread_local! {
static OPTIONS: Cell<JsonParseOptions> = const {
Cell::new(JsonParseOptions {
ignore_unknown_enum_values: false,
strict_extension_keys: false,
})
};
}
pub fn with_json_parse_options<T>(opts: &JsonParseOptions, f: impl FnOnce() -> T) -> T {
let prev = OPTIONS.with(|c| c.replace(*opts));
struct Guard(JsonParseOptions);
impl Drop for Guard {
fn drop(&mut self) {
OPTIONS.with(|c| c.set(self.0));
}
}
let _guard = Guard(prev);
f()
}
pub(crate) fn ignore_unknown_enum_values() -> bool {
OPTIONS.with(|c| c.get().ignore_unknown_enum_values)
}
pub(crate) fn strict_extension_keys() -> bool {
OPTIONS.with(|c| c.get().strict_extension_keys)
}
}
#[cfg(feature = "std")]
pub use std_impl::with_json_parse_options;
#[cfg_attr(feature = "std", allow(dead_code))]
mod global {
use super::JsonParseOptions;
use alloc::boxed::Box;
use once_cell::race::OnceBox;
static OPTS: OnceBox<JsonParseOptions> = OnceBox::new();
static DEFAULT: JsonParseOptions = JsonParseOptions {
ignore_unknown_enum_values: false,
strict_extension_keys: false,
};
pub fn set_global_json_parse_options(opts: &JsonParseOptions) {
if OPTS.set(Box::new(*opts)).is_err() {
let existing = OPTS.get().expect("set() returned Err ⇒ get() is Some");
debug_assert_eq!(
existing, opts,
"set_global_json_parse_options called with options that differ from the \
first call. The first call's options remain in effect. \
(existing: {existing:?}, new: {opts:?})"
);
}
}
#[inline]
pub(crate) fn get() -> &'static JsonParseOptions {
OPTS.get().unwrap_or(&DEFAULT)
}
pub(crate) fn ignore_unknown_enum_values() -> bool {
get().ignore_unknown_enum_values
}
pub(crate) fn strict_extension_keys() -> bool {
get().strict_extension_keys
}
}
#[cfg(not(feature = "std"))]
pub use global::set_global_json_parse_options;
pub(crate) fn ignore_unknown_enum_values() -> bool {
#[cfg(feature = "std")]
{
std_impl::ignore_unknown_enum_values()
}
#[cfg(not(feature = "std"))]
{
global::ignore_unknown_enum_values()
}
}
pub(crate) fn strict_extension_keys() -> bool {
#[cfg(feature = "std")]
{
std_impl::strict_extension_keys()
}
#[cfg(not(feature = "std"))]
{
global::strict_extension_keys()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thread_local_default_does_not_ignore() {
assert!(!ignore_unknown_enum_values());
}
#[test]
fn thread_local_scope_enables_flag() {
let opts = JsonParseOptions {
ignore_unknown_enum_values: true,
..Default::default()
};
with_json_parse_options(&opts, || {
assert!(ignore_unknown_enum_values());
});
assert!(!ignore_unknown_enum_values());
}
#[test]
fn thread_local_nested_scopes_restore_correctly() {
let outer = JsonParseOptions {
ignore_unknown_enum_values: true,
..Default::default()
};
let inner = JsonParseOptions {
ignore_unknown_enum_values: false,
..Default::default()
};
with_json_parse_options(&outer, || {
assert!(ignore_unknown_enum_values());
with_json_parse_options(&inner, || {
assert!(!ignore_unknown_enum_values());
});
assert!(ignore_unknown_enum_values());
});
}
#[test]
fn thread_local_restored_on_panic() {
let opts = JsonParseOptions {
ignore_unknown_enum_values: true,
..Default::default()
};
let result = std::panic::catch_unwind(|| {
with_json_parse_options(&opts, || {
assert!(ignore_unknown_enum_values());
panic!("boom");
});
});
assert!(result.is_err());
assert!(!ignore_unknown_enum_values());
}
#[test]
fn global_set_once_lifecycle() {
assert!(
!global::ignore_unknown_enum_values(),
"unset global should return strict defaults"
);
assert_eq!(*global::get(), JsonParseOptions::default());
let lenient = JsonParseOptions::new().ignore_unknown_enum_values(true);
global::set_global_json_parse_options(&lenient);
assert!(global::ignore_unknown_enum_values());
global::set_global_json_parse_options(&lenient);
global::set_global_json_parse_options(&lenient);
assert!(global::ignore_unknown_enum_values());
let strict = JsonParseOptions::new().ignore_unknown_enum_values(false);
let result = std::thread::spawn(move || {
global::set_global_json_parse_options(&strict);
})
.join();
#[cfg(debug_assertions)]
{
assert!(
result.is_err(),
"mismatch should debug_assert-panic in debug builds"
);
let msg = result.unwrap_err();
let msg_str = msg
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| msg.downcast_ref::<&str>().copied())
.unwrap_or("");
assert!(
msg_str.contains("differ from the first call"),
"expected mismatch diagnostic, got: {msg_str}"
);
}
#[cfg(not(debug_assertions))]
{
assert!(
result.is_ok(),
"release builds silently ignore mismatched second call"
);
}
assert!(
global::ignore_unknown_enum_values(),
"first call's lenient options should remain in effect after mismatch"
);
}
}