use std::fmt::{self, Write as _};
use crate::engine::decode::{DecodedField, DecodedPgn, FieldValue};
use super::{effective_precision, time_precision_for, write_fixed_float};
fn field_display_name(field: &DecodedField, mode: CamelCase) -> std::borrow::Cow<'_, str> {
use std::borrow::Cow;
match mode {
CamelCase::Off => Cow::Borrowed(field.name()),
CamelCase::Lower => Cow::Borrowed(field.id()),
CamelCase::Upper => Cow::Owned(upper_camel(field.id())),
}
}
fn pgn_wrapper_key(pgn: &DecodedPgn, mode: CamelCase) -> std::borrow::Cow<'_, str> {
use std::borrow::Cow;
match mode {
CamelCase::Off => Cow::Borrowed(pgn.description),
CamelCase::Lower => Cow::Borrowed(pgn.id),
CamelCase::Upper => Cow::Owned(upper_camel(pgn.id)),
}
}
fn upper_camel(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
#[derive(Debug, Default, Clone)]
pub struct JsonOptions {
pub include_empty: bool,
pub name_value: bool,
pub debug: bool,
pub camel_case: CamelCase,
pub wrap: bool,
pub protocol: crate::engine::BusProtocol,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CamelCase {
Off,
#[default]
Lower,
Upper,
}
thread_local! {
static FIELDS_BUF: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
}
pub fn write_json<W: fmt::Write>(w: &mut W, pgn: &DecodedPgn, opts: &JsonOptions) -> fmt::Result {
let wrap = opts.wrap;
if wrap {
w.write_char('{')?;
w.write_str("\"")?;
w.write_str(pgn_wrapper_key(pgn, opts.camel_case).as_ref())?;
w.write_str("\":")?;
}
w.write_char('{')?;
if let Some(ts) = &pgn.timestamp {
w.write_str("\"timestamp\":")?;
write_json_string(w, &crate::engine::format::normalize_timestamp(ts))?;
w.write_char(',')?;
}
if opts.protocol.standard_frames() {
write!(w, "\"pgn\":{}", pgn.pgn)?;
} else {
write!(
w,
"\"prio\":{},\"src\":{},\"dst\":{},\"pgn\":{}",
pgn.prio, pgn.src, pgn.dst, pgn.pgn
)?;
}
w.write_str(",\"description\":")?;
write_json_string(w, pgn.description)?;
let mut fields_buf = FIELDS_BUF.with(|c| c.take());
fields_buf.clear();
let result = write_json_inner(w, pgn, opts, &mut fields_buf);
FIELDS_BUF.with(|c| *c.borrow_mut() = fields_buf);
result?;
if wrap {
w.write_char('}')?;
}
Ok(())
}
fn write_json_inner<W: fmt::Write>(
w: &mut W,
pgn: &DecodedPgn,
opts: &JsonOptions,
fields_buf: &mut String,
) -> fmt::Result {
let mut top_sep = "";
let mut current_set: u8 = 0;
let mut current_iter: Option<u32> = None;
let payload_bits = (pgn.data.len() as u32).saturating_mul(8);
for f in &pgn.fields {
if let Some(bo) = f.bit_offset
&& bo >= payload_bits
{
continue;
}
if !opts.include_empty && !opts.debug && f.value.is_sentinel() {
continue;
}
if let FieldValue::Spare { value, .. } = &f.value
&& *value == 0
{
continue;
}
if matches!(&f.value, FieldValue::Reserved { bytes, .. } if bytes.is_empty()) {
continue;
}
if !opts.include_empty
&& !opts.debug
&& let FieldValue::BitField { bits, .. } = &f.value
&& bits.is_empty()
{
continue;
}
if f.repeat_set == 0 {
if current_set != 0 {
fields_buf.write_str("}]")?;
current_set = 0;
current_iter = None;
}
fields_buf.write_str(top_sep)?;
write_json_string(fields_buf, field_display_name(f, opts.camel_case).as_ref())?;
fields_buf.write_char(':')?;
write_field_value(fields_buf, f, opts, &pgn.data)?;
top_sep = ",";
} else {
let iter = f.repeat_index.unwrap_or(0);
if current_set != f.repeat_set {
if current_set != 0 {
fields_buf.write_str("}]")?;
}
fields_buf.write_str(top_sep)?;
let key = if f.repeat_set == 1 {
"\"list\":[{"
} else {
"\"list2\":[{"
};
fields_buf.write_str(key)?;
current_set = f.repeat_set;
current_iter = Some(iter);
top_sep = ",";
write_json_string(fields_buf, field_display_name(f, opts.camel_case).as_ref())?;
fields_buf.write_char(':')?;
write_field_value(fields_buf, f, opts, &pgn.data)?;
} else if Some(iter) != current_iter {
fields_buf.write_str("},{")?;
current_iter = Some(iter);
write_json_string(fields_buf, field_display_name(f, opts.camel_case).as_ref())?;
fields_buf.write_char(':')?;
write_field_value(fields_buf, f, opts, &pgn.data)?;
} else {
fields_buf.write_char(',')?;
write_json_string(fields_buf, field_display_name(f, opts.camel_case).as_ref())?;
fields_buf.write_char(':')?;
write_field_value(fields_buf, f, opts, &pgn.data)?;
}
}
}
if current_set != 0 {
fields_buf.write_str("}]")?;
}
let renderable = |f: &DecodedField| {
if let FieldValue::Spare { value, .. } = &f.value
&& *value == 0
{
return false;
}
if matches!(f.value, FieldValue::NotAvailable) && !opts.include_empty && !opts.debug {
return false;
}
true
};
let saw_set1 = pgn
.fields
.iter()
.any(|f| f.repeat_set == 1 && renderable(f));
let saw_set2 = pgn
.fields
.iter()
.any(|f| f.repeat_set == 2 && renderable(f));
if pgn.has_repeating_set[0] && !saw_set1 {
if !fields_buf.is_empty() && current_set == 0 {
fields_buf.push_str(top_sep);
}
fields_buf.push_str("\"list\":[{}]");
}
if pgn.has_repeating_set[1] && !saw_set2 {
if !fields_buf.is_empty() {
fields_buf.push(',');
}
fields_buf.push_str("\"list2\":[{}]");
}
if !fields_buf.is_empty() {
w.write_str(",\"fields\":{")?;
w.write_str(fields_buf)?;
w.write_char('}')?;
}
w.write_char('}')?; Ok(())
}
fn write_debug_suffix<W: fmt::Write>(w: &mut W, f: &DecodedField, payload: &[u8]) -> fmt::Result {
let (Some(bo), Some(bl)) = (f.bit_offset, f.bit_length) else {
return Ok(());
};
if bl == 0 {
return Ok(());
}
if bo % 8 == 0 && bl % 8 == 0 {
let start = (bo / 8) as usize;
let end = ((bo + bl) / 8) as usize;
let end_clamped = end.min(payload.len());
if start < end_clamped {
w.write_str(",\"bytes\":\"")?;
for (i, b) in payload[start..end_clamped].iter().enumerate() {
if i > 0 {
w.write_char(' ')?;
}
write!(w, "{:02X}", b)?;
}
w.write_char('"')?;
}
return Ok(());
}
use crate::engine::bits::extract_bits;
let signed = matches!(
f.value,
FieldValue::Number(_) if f.unit().is_some()
);
let Some(ex) = extract_bits(payload, bo as usize, bl as usize, signed, 0) else {
return Ok(());
};
let raw_unsigned = ex.value as u64;
let shift = bo % 8;
let byte_span = ((shift + bl).div_ceil(8)) as usize;
let shifted: u128 = (raw_unsigned as u128) << shift;
w.write_str(",\"bytes\":\"")?;
for i in 0..byte_span {
if i > 0 {
w.write_char(' ')?;
}
let byte = ((shifted >> (i * 8)) & 0xff) as u8;
write!(w, "{:02X}", byte)?;
}
w.write_char('"')?;
if bl % 8 != 0 {
w.write_str(",\"bits\":\"")?;
for i in (0..bl).rev() {
let byte = (raw_unsigned >> (i / 8)) & 0xff;
let bit = (byte >> (i % 8)) & 1;
w.write_char(if bit == 1 { '1' } else { '0' })?;
}
w.write_char('"')?;
}
Ok(())
}
fn write_field_value_debug<W: fmt::Write>(
w: &mut W,
f: &DecodedField,
opts: &JsonOptions,
payload: &[u8],
) -> fmt::Result {
w.write_char('{')?;
w.write_str("\"value\":")?;
match &f.value {
FieldValue::Decimal(d) => write_json_string(w, d)?,
FieldValue::Number(v) => {
let p = effective_precision(f.precision(), f.resolution());
let min_w = if p == 7 && f.unit() == Some("deg") {
10
} else {
0
};
write_fixed_float(w, *v, p, min_w)?;
}
FieldValue::Integer(v) => write!(w, "{}", v)?,
FieldValue::Float(v) => write!(w, "{}", v)?,
FieldValue::Binary(bytes) => {
w.write_char('"')?;
for (i, b) in bytes.iter().enumerate() {
if i > 0 {
w.write_char(' ')?;
}
write!(w, "{:02X}", b)?;
}
w.write_char('"')?;
}
FieldValue::Lookup { value, name } => {
if opts.name_value {
write!(w, "{},\"name\":", value)?;
match name {
Some(n) => write_json_string(w, n)?,
None => w.write_str("null")?,
}
} else {
match name {
Some(n) => write_json_string(w, n)?,
None => write!(w, "{}", value)?,
}
}
}
FieldValue::BitField { value, bits } => {
if bits.is_empty() {
let _ = value;
w.write_str("null")?;
} else if opts.name_value {
w.write_char('[')?;
for (i, (bv, n)) in bits.iter().enumerate() {
if i > 0 {
w.write_char(',')?;
}
write!(w, "{{\"value\":{},\"name\":", bv)?;
match n {
Some(n) => write_json_string(w, n)?,
None => w.write_str("null")?,
}
w.write_char('}')?;
}
w.write_char(']')?;
} else {
w.write_char('[')?;
for (i, (bv, n)) in bits.iter().enumerate() {
if i > 0 {
w.write_char(',')?;
}
match n {
Some(n) => write_json_string(w, n)?,
None => write!(w, "{}", bv)?,
}
}
w.write_char(']')?;
}
}
FieldValue::String(s) => write_field_json_string(w, s)?,
FieldValue::Date(d) => {
let mut buf = String::with_capacity(10);
super::format_date(*d, &mut buf)?;
if opts.name_value {
write!(w, "{}", d)?;
w.write_str(",\"name\":")?;
write_json_string(w, &buf)?;
} else {
write_json_string(w, &buf)?;
}
}
FieldValue::Time { seconds, .. } => {
let p = time_precision_for(f.resolution());
let mut buf = String::with_capacity(12);
super::format_time(*seconds, p, false, &mut buf)?;
write_fixed_float(w, *seconds, p, 0)?;
if opts.name_value {
w.write_str(",\"name\":")?;
write_json_string(w, &buf)?;
}
}
FieldValue::Mmsi(v) => write!(w, "\"{:09}\"", v)?,
FieldValue::Pgn { value, description } => {
write!(w, "{}", value)?;
if opts.name_value {
w.write_str(",\"name\":")?;
match description {
Some(desc) => write_json_string(w, desc)?,
None => w.write_str("null")?,
}
}
}
FieldValue::IsoName { value, subfields } => {
write!(w, "{}", value)?;
if opts.name_value {
let name_payload: &[u8] = match f.bit_offset {
Some(bo) if bo % 8 == 0 && (bo / 8) as usize + 8 <= payload.len() => {
let s = (bo / 8) as usize;
&payload[s..s + 8]
}
_ => &[],
};
w.write_str(",\"name\":{")?;
let mut sep = "";
for sf in subfields {
if !opts.include_empty && matches!(sf.value, FieldValue::NotAvailable) {
continue;
}
if matches!(&sf.value, FieldValue::Spare { value, .. } if *value == 0) {
continue;
}
w.write_str(sep)?;
write_json_string(w, field_display_name(sf, opts.camel_case).as_ref())?;
w.write_char(':')?;
write_field_value_debug(w, sf, opts, name_payload)?;
sep = ",";
}
w.write_char('}')?;
}
}
FieldValue::Reserved { bytes, .. } => {
w.write_char('"')?;
for (i, b) in bytes.iter().enumerate() {
if i > 0 {
w.write_char(' ')?;
}
write!(w, "{:02X}", b)?;
}
w.write_char('"')?;
}
FieldValue::Spare { .. } | FieldValue::NotAvailable => {
w.write_str("null")?;
}
FieldValue::OutOfRange { .. } | FieldValue::ReservedValue { .. } => {
w.write_str("null")?;
}
FieldValue::Unsupported { field_type } => {
let mut buf = String::with_capacity(field_type.len() + 16);
buf.push_str("<unsupported:");
buf.push_str(field_type);
buf.push('>');
write_json_string(w, &buf)?;
}
}
write_debug_suffix(w, f, payload)?;
if opts.name_value && f.part_of_primary_key() {
w.write_str(",\"key\":true")?;
}
w.write_char('}')
}
fn write_field_value<W: fmt::Write>(
w: &mut W,
f: &DecodedField,
opts: &JsonOptions,
payload: &[u8],
) -> fmt::Result {
if opts.debug {
return write_field_value_debug(w, f, opts, payload);
}
match &f.value {
FieldValue::Number(v) => {
let p = effective_precision(f.precision(), f.resolution());
let min_w = if p == 7 && f.unit() == Some("deg") {
10
} else {
0
};
write_fixed_float(w, *v, p, min_w)
}
FieldValue::Integer(v) => {
if opts.name_value && f.part_of_primary_key() {
write!(w, "{{\"value\":{},\"key\":true}}", v)
} else {
write!(w, "{}", v)
}
}
FieldValue::Float(v) => super::write_c_g(w, *v),
FieldValue::Binary(bytes) => {
w.write_char('"')?;
for (i, b) in bytes.iter().enumerate() {
if i > 0 {
w.write_char(' ')?;
}
write!(w, "{:02X}", b)?;
}
w.write_char('"')
}
FieldValue::Lookup { value, name } => {
if opts.name_value {
w.write_char('{')?;
write!(w, "\"value\":{}", value)?;
match (name, opts.include_empty) {
(Some(n), _) => {
w.write_str(",\"name\":")?;
write_json_string(w, n)?;
}
(None, true) => w.write_str(",\"name\":null")?,
(None, false) => {}
}
if f.part_of_primary_key() {
w.write_str(",\"key\":true")?;
}
w.write_char('}')
} else {
match name {
Some(n) => write_json_string(w, n),
None => write!(w, "{}", value),
}
}
}
FieldValue::BitField { bits, value } => {
if bits.is_empty() {
let _ = value;
w.write_str("null")
} else if opts.name_value {
w.write_char('[')?;
for (i, (bv, n)) in bits.iter().enumerate() {
if i > 0 {
w.write_char(',')?;
}
write!(w, "{{\"value\":{},\"name\":", bv)?;
match n {
Some(n) => write_json_string(w, n)?,
None => w.write_str("null")?,
}
w.write_char('}')?;
}
w.write_char(']')
} else {
w.write_char('[')?;
for (i, (bv, n)) in bits.iter().enumerate() {
if i > 0 {
w.write_char(',')?;
}
match n {
Some(n) => write_json_string(w, n)?,
None => write!(w, "{}", bv)?,
}
}
w.write_char(']')
}
}
FieldValue::Decimal(d) => write_json_string(w, d),
FieldValue::String(s) => write_field_json_string(w, s),
FieldValue::Date(d) => {
let mut buf = String::with_capacity(10);
super::format_date(*d, &mut buf)?;
if opts.name_value {
w.write_str("{\"value\":")?;
write!(w, "{}", d)?;
w.write_str(",\"name\":")?;
write_json_string(w, &buf)?;
w.write_char('}')
} else {
write_json_string(w, &buf)
}
}
FieldValue::Time { seconds, .. } => {
let p = time_precision_for(f.resolution());
let mut buf = String::with_capacity(12);
super::format_time(*seconds, p, false, &mut buf)?;
if opts.name_value {
w.write_str("{\"value\":")?;
write_fixed_float(w, *seconds, p, 0)?;
w.write_str(",\"name\":")?;
write_json_string(w, &buf)?;
w.write_char('}')
} else {
write_fixed_float(w, *seconds, p, 0)
}
}
FieldValue::Mmsi(v) => {
if opts.name_value && f.part_of_primary_key() {
write!(w, "{{\"value\":\"{:09}\",\"key\":true}}", v)
} else {
write!(w, "\"{:09}\"", v)
}
}
FieldValue::Pgn { value, description } => {
if opts.name_value {
w.write_char('{')?;
write!(w, "\"value\":{}", value)?;
if let Some(desc) = description {
w.write_str(",\"name\":")?;
write_json_string(w, desc)?;
}
w.write_char('}')
} else {
write!(w, "{}", value)
}
}
FieldValue::Reserved { bytes, .. } => {
w.write_char('"')?;
for (i, b) in bytes.iter().enumerate() {
if i > 0 {
w.write_char(' ')?;
}
write!(w, "{:02X}", b)?;
}
w.write_char('"')
}
FieldValue::Spare { bytes, .. } => {
w.write_char('"')?;
for (i, b) in bytes.iter().enumerate() {
if i > 0 {
w.write_char(' ')?;
}
write!(w, "{:02X}", b)?;
}
w.write_char('"')
}
FieldValue::IsoName { value, subfields } => {
if opts.name_value {
w.write_char('{')?;
write!(w, "\"value\":{}", value)?;
w.write_str(",\"name\":{")?;
let mut sep = "";
for sf in subfields {
if !opts.include_empty && matches!(sf.value, FieldValue::NotAvailable) {
continue;
}
if matches!(&sf.value, FieldValue::Spare { value, .. } if *value == 0) {
continue;
}
w.write_str(sep)?;
write_json_string(w, field_display_name(sf, opts.camel_case).as_ref())?;
w.write_char(':')?;
write_field_value(w, sf, opts, payload)?;
sep = ",";
}
w.write_char('}')?;
w.write_char('}')
} else {
write!(w, "{}", value)
}
}
FieldValue::NotAvailable
| FieldValue::OutOfRange { .. }
| FieldValue::ReservedValue { .. } => w.write_str("null"),
FieldValue::Unsupported { field_type } => {
let mut buf = String::with_capacity(field_type.len() + 16);
buf.push_str("<unsupported:");
buf.push_str(field_type);
buf.push('>');
write_json_string(w, &buf)
}
}
}
const ESCAPE_TABLE: [Option<&str>; 256] = build_escape_table(false);
const ESCAPE_TABLE_WITH_SLASH: [Option<&str>; 256] = build_escape_table(true);
const fn build_escape_table(slash: bool) -> [Option<&'static str>; 256] {
let mut t: [Option<&'static str>; 256] = [None; 256];
t[b'"' as usize] = Some("\\\"");
t[b'\\' as usize] = Some("\\\\");
t[0x08] = Some("\\b");
t[0x09] = Some("\\t");
t[0x0a] = Some("\\n");
t[0x0c] = Some("\\f");
t[0x0d] = Some("\\r");
let mut i = 0u8;
while i < 0x20 {
if t[i as usize].is_none() {
t[i as usize] = Some("*"); }
i += 1;
}
if slash {
t[b'/' as usize] = Some("\\/");
}
t
}
fn write_json_string<W: fmt::Write>(w: &mut W, s: &str) -> fmt::Result {
write_json_string_with_table(w, s, &ESCAPE_TABLE)
}
fn write_field_json_string<W: fmt::Write>(w: &mut W, s: &str) -> fmt::Result {
write_json_string_with_table(w, s, &ESCAPE_TABLE_WITH_SLASH)
}
#[inline]
fn write_json_string_with_table<W: fmt::Write>(
w: &mut W,
s: &str,
table: &[Option<&str>; 256],
) -> fmt::Result {
w.write_char('"')?;
let bytes = s.as_bytes();
let mut start = 0usize;
for (i, &b) in bytes.iter().enumerate() {
if let Some(esc) = table[b as usize] {
if i > start {
w.write_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..i]) })?;
}
if esc.as_bytes() == b"*" {
write!(w, "\\u{:04x}", b as u32)?;
} else {
w.write_str(esc)?;
}
start = i + 1;
}
}
if start < bytes.len() {
w.write_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..]) })?;
}
w.write_char('"')?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_quick_record_has_no_prio_src_or_dst() {
let db = crate::engine::PgnDatabase::embedded_quick(crate::engine::Units::Si);
let frame =
crate::engine::RawFrame::new(None, 0, 0x6c1, 0, 255, [0xc1, 0x18, 0x6a, 0, 0, 0, 2, 0]);
let pgn = db.decode(&frame).unwrap();
let json = |protocol| {
let mut out = String::new();
let opts = JsonOptions {
protocol,
..Default::default()
};
write_json(&mut out, &pgn, &opts).unwrap();
out
};
let quick = json(crate::engine::BusProtocol::Quick);
assert!(
quick.starts_with(r#"{"pgn":1729,"description":"Quick: Chain Count""#),
"{quick}"
);
let n2k = json(crate::engine::BusProtocol::Nmea2000);
assert!(
n2k.starts_with(r#"{"prio":0,"src":0,"dst":255,"pgn":1729,"#),
"{n2k}"
);
}
#[test]
fn a_decimal_is_a_json_string() {
let data: Vec<u8> =
"74,6c,00,16,29,02,28,64,7e,39,30,30,30,31,36,ff,ff,ff,ff,ff,ff,02,01,ff,ff,ff,\
7f,ff,ff,ff,7f,ff,ff,ff,ff,ff,ff,ff,ff,ff,7f,fc,ff,ff,ff,ff,ff,ff,ff,ff,ff,ff,\
ff,ff,40,f2,b5,17,d4,34,00,00"
.split(',')
.map(|h| u8::from_str_radix(h.trim(), 16).unwrap())
.collect();
let frame = crate::engine::RawFrame::new(None, 4, 129808, 4, 255, data);
let pgn = crate::engine::PgnDatabase::embedded(crate::engine::Units::Si)
.decode(&frame)
.unwrap();
for opts in [
JsonOptions::default(),
JsonOptions {
debug: true,
..Default::default()
},
] {
let mut out = String::new();
write_json(&mut out, &pgn, &opts).unwrap();
assert!(out.contains("\"002241024\""), "{out}");
assert!(!out.contains(":002241024"), "bare DECIMAL in {out}");
}
}
use crate::engine::decode::{DecodedField, FieldValue};
fn sample_pgn() -> DecodedPgn {
let info = crate::engine::PgnDatabase::embedded(crate::engine::Units::Metric)
.first_pgn(128267)
.expect("PGN 128267 present");
let offset_field = &info.fields[2];
DecodedPgn {
timestamp: Some("2018-10-16T22:25:25.166".into()),
prio: 3,
pgn: 128267,
src: 35,
dst: 255,
description: "Water Depth",
id: "waterDepth",
id_is_pinned: false,
data: Vec::new(),
fields: vec![DecodedField {
info: offset_field,
value: FieldValue::Number(0.0),
bit_offset: None,
bit_length: None,
repeat_index: None,
repeat_set: 0,
overrides: None,
}],
has_repeating_set: [false, false],
index_by_order: [i8::MIN; 32],
}
}
#[test]
fn default_shape_is_flat_camel_case() {
let pgn = sample_pgn();
let mut out = String::new();
write_json(&mut out, &pgn, &JsonOptions::default()).unwrap();
assert_eq!(
out,
r#"{"timestamp":"2018-10-16T22:25:25.166Z","prio":3,"src":35,"dst":255,"pgn":128267,"description":"Water Depth","fields":{"offset":0.000}}"#
);
}
#[test]
fn matches_canboat_json_shape() {
let pgn = sample_pgn();
let mut out = String::new();
let opts = JsonOptions {
camel_case: CamelCase::Off,
..JsonOptions::default()
};
write_json(&mut out, &pgn, &opts).unwrap();
assert_eq!(
out,
r#"{"timestamp":"2018-10-16T22:25:25.166Z","prio":3,"src":35,"dst":255,"pgn":128267,"description":"Water Depth","fields":{"Offset":0.000}}"#
);
}
#[test]
fn camel_mode_keeps_the_human_description() {
let pgn = sample_pgn();
let mut out = String::new();
let opts = JsonOptions {
camel_case: CamelCase::Lower,
wrap: true,
..JsonOptions::default()
};
write_json(&mut out, &pgn, &opts).unwrap();
assert_eq!(
out,
r#"{"waterDepth":{"timestamp":"2018-10-16T22:25:25.166Z","prio":3,"src":35,"dst":255,"pgn":128267,"description":"Water Depth","fields":{"offset":0.000}}}"#
);
}
#[test]
fn wrapping_is_independent_of_identifier_style() {
let pgn = sample_pgn();
let mut out = String::new();
let opts = JsonOptions {
camel_case: CamelCase::Off,
wrap: true,
..JsonOptions::default()
};
write_json(&mut out, &pgn, &opts).unwrap();
assert!(out.starts_with(r#"{"Water Depth":{"#), "got: {out}");
assert!(
out.ends_with(r#""fields":{"Offset":0.000}}}"#),
"got: {out}"
);
}
#[test]
fn fields_object_omitted_when_all_unavailable() {
let mut pgn = sample_pgn();
pgn.fields[0].value = FieldValue::NotAvailable;
let mut out = String::new();
write_json(&mut out, &pgn, &JsonOptions::default()).unwrap();
assert!(
!out.contains("\"fields\""),
"fields wrapper leaked through: {out}"
);
assert!(out.ends_with("}"), "got: {out}");
}
#[test]
fn include_empty_emits_null() {
let mut pgn = sample_pgn();
pgn.fields[0].value = FieldValue::NotAvailable;
let mut out = String::new();
write_json(
&mut out,
&pgn,
&JsonOptions {
include_empty: true,
..Default::default()
},
)
.unwrap();
assert!(out.contains(r#""offset":null"#), "got: {}", out);
}
#[test]
fn out_of_range_and_reserved_are_dropped_from_json() {
for sentinel in [
FieldValue::OutOfRange { value: 254 },
FieldValue::ReservedValue { value: 253 },
] {
let mut pgn = sample_pgn();
pgn.fields[0].value = sentinel;
let mut out = String::new();
write_json(&mut out, &pgn, &JsonOptions::default()).unwrap();
assert!(!out.contains("\"fields\""), "sentinel leaked: {out}");
}
}
#[test]
fn include_empty_spells_every_sentinel_null() {
for sentinel in [
FieldValue::OutOfRange { value: 254 },
FieldValue::ReservedValue { value: 253 },
] {
let mut pgn = sample_pgn();
pgn.fields[0].value = sentinel;
let mut out = String::new();
write_json(
&mut out,
&pgn,
&JsonOptions {
include_empty: true,
..Default::default()
},
)
.unwrap();
assert!(out.contains(r#""offset":null"#), "got: {out}");
}
}
#[test]
fn name_value_emits_object_for_lookup() {
let mut pgn = sample_pgn();
pgn.fields[0].value = FieldValue::Lookup {
value: 275,
name: Some("Navico"),
};
pgn.fields[0].overrides = Some(Box::new(crate::engine::decode::FieldOverrides {
unit: None,
resolution: Some(1.0),
precision: 0,
}));
let mut out = String::new();
write_json(
&mut out,
&pgn,
&JsonOptions {
name_value: true,
..Default::default()
},
)
.unwrap();
assert!(
out.contains(r#""offset":{"value":275,"name":"Navico"}"#),
"got: {}",
out
);
}
#[test]
fn escapes_quotes_in_strings() {
let mut pgn = sample_pgn();
pgn.description = r#"He said "hi""#;
let mut out = String::new();
write_json(&mut out, &pgn, &JsonOptions::default()).unwrap();
assert!(
out.contains(r#""description":"He said \"hi\"""#),
"got: {}",
out
);
}
}