use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};
use chrono::{DateTime, Utc};
use serde_json::{Map, Number, Value};
#[cfg(test)]
mod tests;
pub(crate) fn stringify(value: &Value) -> String {
let mut out = String::new();
let _ = write_stringify(value, &mut out);
out
}
pub(crate) fn write_stringify(value: &Value, out: &mut impl fmt::Write) -> fmt::Result {
match value {
Value::Null => out.write_str("null"),
Value::Bool(b) => out.write_str(if *b { "true" } else { "false" }),
Value::Number(n) => write_number(out, n),
Value::String(s) => write_quoted(out, s),
Value::Array(items) => {
out.write_char('[')?;
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.write_char(',')?;
}
write_stringify(item, out)?;
}
out.write_char(']')
}
Value::Object(object) => {
out.write_char('{')?;
for (i, (key, item)) in own_entries(object).enumerate() {
if i > 0 {
out.write_char(',')?;
}
write_quoted(out, key)?;
out.write_char(':')?;
write_stringify(item, out)?;
}
out.write_char('}')
}
}
}
pub(crate) fn write_quoted(out: &mut impl fmt::Write, text: &str) -> fmt::Result {
const HEX: &[u8; 16] = b"0123456789abcdef";
out.write_char('"')?;
let mut copied = 0;
for (at, byte) in text.bytes().enumerate() {
let escape = match byte {
b'"' => "\\\"",
b'\\' => "\\\\",
0x08 => "\\b",
b'\t' => "\\t",
b'\n' => "\\n",
0x0C => "\\f",
b'\r' => "\\r",
0x00..=0x1F => "\\u00",
_ => continue,
};
out.write_str(&text[copied..at])?;
out.write_str(escape)?;
if escape == "\\u00" {
out.write_char(char::from(HEX[usize::from(byte >> 4)]))?;
out.write_char(char::from(HEX[usize::from(byte & 0x0F)]))?;
}
copied = at + 1;
}
out.write_str(&text[copied..])?;
out.write_char('"')
}
pub(crate) fn as_published(value: &Value) -> Value {
serde_json::from_str(&stringify(value)).unwrap_or_else(|_| value.clone())
}
#[cfg_attr(
not(test),
expect(dead_code, reason = "only the tests read the number text on its own")
)]
pub(crate) fn number_to_string(number: &Number) -> String {
let mut out = String::new();
let _ = write_number(&mut out, number);
out
}
fn write_number(out: &mut impl fmt::Write, number: &Number) -> fmt::Result {
match number.as_f64() {
Some(f) => out.write_str(ryu_js::Buffer::new().format(f)),
None => write!(out, "{number}"),
}
}
pub(crate) fn own_entries(object: &Map<String, Value>) -> OwnEntries<'_> {
let mut rest = object.iter();
let mut before = 0;
let first = loop {
let Some(entry) = rest.next() else {
return OwnEntries::Insertion(object.iter());
};
if let Some(index) = array_index(entry.0) {
break (index, entry);
}
before += 1;
};
let mut indices = vec![first];
let mut others = Vec::with_capacity(object.len());
others.extend(object.iter().take(before));
for entry in rest {
match array_index(entry.0) {
Some(index) => indices.push((index, entry)),
None => others.push(entry),
}
}
indices.sort_unstable_by_key(|&(index, _)| index);
let ordered: Vec<(&String, &Value)> = indices
.into_iter()
.map(|(_, entry)| entry)
.chain(others)
.collect();
OwnEntries::Reordered(ordered.into_iter())
}
pub(crate) enum OwnEntries<'a> {
Insertion(serde_json::map::Iter<'a>),
Reordered(std::vec::IntoIter<(&'a String, &'a Value)>),
}
impl<'a> Iterator for OwnEntries<'a> {
type Item = (&'a String, &'a Value);
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Insertion(entries) => entries.next(),
Self::Reordered(entries) => entries.next(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
Self::Insertion(entries) => entries.size_hint(),
Self::Reordered(entries) => entries.size_hint(),
}
}
}
impl ExactSizeIterator for OwnEntries<'_> {}
fn array_index(key: &str) -> Option<u32> {
let canonical = match key.as_bytes() {
[b'0'] => true,
[b'1'..=b'9', rest @ ..] => rest.len() < 10 && rest.iter().all(u8::is_ascii_digit),
_ => false,
};
if !canonical {
return None;
}
key.parse::<u32>().ok().filter(|&index| index != u32::MAX)
}
pub(crate) fn utf16_len(text: &str) -> usize {
text.chars().map(char::len_utf16).sum()
}
pub(crate) fn cut_utf16(text: &str, units: usize) -> &str {
if text.len() <= units {
return text;
}
let mut used = 0;
for (at, c) in text.char_indices() {
used += c.len_utf16();
if used > units {
return &text[..at];
}
}
text
}
pub(crate) fn trim(text: &str) -> &str {
text.trim_matches(is_trim_whitespace)
}
fn is_trim_whitespace(c: char) -> bool {
matches!(
c,
'\u{9}'..='\u{D}'
| ' '
| '\u{A0}'
| '\u{1680}'
| '\u{2000}'..='\u{200A}'
| '\u{2028}'
| '\u{2029}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
| '\u{FEFF}'
)
}
pub(crate) fn coerce_to_string(value: Option<&Value>) -> String {
let mut out = String::new();
match value {
None => out.push_str("undefined"),
Some(value) => push_coerced_string(&mut out, value),
}
out
}
fn push_coerced_string(out: &mut String, value: &Value) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(n) => {
let _ = write_number(out, n);
}
Value::String(s) => out.push_str(s),
Value::Array(items) => {
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
if !item.is_null() {
push_coerced_string(out, item);
}
}
}
Value::Object(_) => out.push_str("[object Object]"),
}
}
pub(crate) fn truthy(value: &Value) -> bool {
match value {
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
Value::String(s) => !s.is_empty(),
Value::Array(_) | Value::Object(_) => true,
}
}
pub(crate) fn type_of(value: Option<&Value>) -> &'static str {
match value {
None => "undefined",
Some(Value::Bool(_)) => "boolean",
Some(Value::Number(_)) => "number",
Some(Value::String(_)) => "string",
Some(Value::Null | Value::Array(_) | Value::Object(_)) => "object",
}
}
pub(crate) fn iso_millis(time: SystemTime) -> String {
let (secs, nanos) = match time.duration_since(UNIX_EPOCH) {
Ok(after) => (i64::try_from(after.as_secs()).ok(), after.subsec_nanos()),
Err(error) => {
let before = error.duration();
match (before.as_secs(), before.subsec_nanos()) {
(secs, 0) => (i64::try_from(secs).ok().map(|s| -s), 0),
(secs, nanos) => (
i64::try_from(secs).ok().map(|s| -s - 1),
1_000_000_000 - nanos,
),
}
}
};
let date = secs
.and_then(|secs| DateTime::<Utc>::from_timestamp(secs, nanos))
.unwrap_or(DateTime::UNIX_EPOCH);
date.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
}
pub(crate) fn parse_iso_millis(text: &str) -> Option<SystemTime> {
DateTime::parse_from_rfc3339(text)
.ok()
.map(|time| SystemTime::from(time.with_timezone(&Utc)))
}