use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
assets::{MAP_W, SHEET_W},
pico8::{bytes_to_hex, hex_bytes},
};
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Versioned<T> {
pub version: u32,
#[serde(flatten)]
pub inner: T,
}
pub(crate) fn to_readable_json<T>(value: &T) -> serde_json::Result<String>
where
T: Serialize,
{
let v = serde_json::to_value(value)?;
let mut out = String::new();
write_value(&v, 0, &mut out);
out.push('\n');
Ok(out)
}
pub(crate) mod pixel_rows {
use super::*;
use serde::{Deserializer, Serializer};
pub fn serialize<S>(px: &[u8], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let rows: Vec<String> = px.chunks(SHEET_W).map(nibbles_to_hex).collect();
rows.serialize(s)
}
pub fn deserialize<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let rows = Vec::<String>::deserialize(d)?;
Ok(rows.iter().flat_map(|r| hex_to_nibbles(r)).collect())
}
}
pub(crate) mod pixel_rows_opt {
use super::*;
use serde::{Deserializer, Serializer};
pub fn serialize<S>(px: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match px {
Some(px) => {
let rows: Vec<String> = px.chunks(SHEET_W).map(nibbles_to_hex).collect();
s.serialize_some(&rows)
}
None => s.serialize_none(),
}
}
pub fn deserialize<'de, D>(d: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
let opt = Option::<Vec<String>>::deserialize(d)?;
Ok(opt.map(|rows| rows.iter().flat_map(|r| hex_to_nibbles(r)).collect()))
}
}
pub(crate) mod tile_rows {
use super::*;
use serde::{Deserializer, Serializer};
pub fn serialize<S>(tiles: &[u8], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let rows: Vec<String> = tiles.chunks(MAP_W).map(bytes_to_hex).collect();
rows.serialize(s)
}
pub fn deserialize<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let rows = Vec::<String>::deserialize(d)?;
Ok(rows.iter().flat_map(|r| hex_bytes(r)).collect())
}
}
pub(crate) mod hex_string {
use super::*;
use serde::{Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
bytes_to_hex(bytes).serialize(s)
}
pub fn deserialize<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
Ok(hex_bytes(&String::deserialize(d)?))
}
}
pub(crate) mod base64_bytes {
use super::*;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use serde::{de::Error as _, Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
STANDARD.encode(bytes).serialize(s)
}
pub fn deserialize<'de, D>(d: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(d)?;
STANDARD.decode(s).map_err(D::Error::custom)
}
}
fn nibbles_to_hex(cells: &[u8]) -> String {
cells
.iter()
.map(|&c| char::from_digit((c & 0x0f) as u32, 16).unwrap())
.collect()
}
fn hex_to_nibbles(s: &str) -> Vec<u8> {
s.chars()
.filter_map(|c| c.to_digit(16))
.map(|d| d as u8)
.collect()
}
const MAX_INLINE: usize = 512;
fn write_value(v: &Value, indent: usize, out: &mut String) {
let inline = match v {
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => true,
Value::Array(_) => v.to_string().len() <= MAX_INLINE,
Value::Object(_) => false,
};
if inline {
out.push_str(&v.to_string());
return;
}
match v {
Value::Array(a) => {
out.push_str("[\n");
for (i, e) in a.iter().enumerate() {
push_indent(out, indent + 1);
write_value(e, indent + 1, out);
if i + 1 < a.len() {
out.push(',');
}
out.push('\n');
}
push_indent(out, indent);
out.push(']');
}
Value::Object(o) => {
out.push_str("{\n");
for (i, (k, e)) in o.iter().enumerate() {
push_indent(out, indent + 1);
out.push_str(&Value::String(k.clone()).to_string());
out.push_str(": ");
write_value(e, indent + 1, out);
if i + 1 < o.len() {
out.push(',');
}
out.push('\n');
}
push_indent(out, indent);
out.push('}');
}
_ => unreachable!("scalars handled above"),
}
}
fn push_indent(out: &mut String, indent: usize) {
for _ in 0..indent {
out.push_str(" ");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Grid {
#[serde(with = "pixel_rows")]
px: Vec<u8>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct OptionalGrid {
#[serde(with = "pixel_rows_opt", default)]
px: Option<Vec<u8>>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Tiles {
#[serde(with = "tile_rows")]
t: Vec<u8>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Flags {
#[serde(with = "hex_string")]
f: Vec<u8>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Wasm {
#[serde(with = "base64_bytes")]
w: Vec<u8>,
}
#[test]
fn pixel_rows_encode_as_nibble_strings_and_round_trip() {
let mut px = vec![0u8; SHEET_W * 2];
px[0] = 0x0f;
px[SHEET_W] = 0x0a;
let json = serde_json::to_string(&Grid { px: px.clone() }).unwrap();
assert!(json.contains("\"f0"), "row 0 starts with f: {json}");
assert!(json.contains("\"a0"), "row 1 starts with a: {json}");
assert_eq!(serde_json::from_str::<Grid>(&json).unwrap().px, px);
}
#[test]
fn pixel_rows_opt_round_trips_some_and_none() {
let mut px = vec![0u8; SHEET_W + 1];
px[0] = 0x0f;
px[SHEET_W] = 0x0a;
let json = serde_json::to_string(&OptionalGrid {
px: Some(px.clone()),
})
.unwrap();
assert!(json.contains("\"f0"), "row 0 starts with f: {json}");
assert!(json.contains("\"a\""), "row 1 is a lone nibble: {json}");
assert_eq!(
serde_json::from_str::<OptionalGrid>(&json).unwrap().px,
Some(px)
);
let json = serde_json::to_string(&OptionalGrid { px: None }).unwrap();
assert!(json.contains("null"), "{json}");
assert_eq!(
serde_json::from_str::<OptionalGrid>(&json).unwrap().px,
None
);
}
#[test]
fn tile_rows_encode_as_byte_strings_and_round_trip() {
let mut t = vec![0u8; MAP_W * 2];
t[0] = 0x2a;
let json = serde_json::to_string(&Tiles { t: t.clone() }).unwrap();
assert!(json.contains("\"2a00"), "tile row: {json}");
assert_eq!(serde_json::from_str::<Tiles>(&json).unwrap().t, t);
}
#[test]
fn flags_round_trip_as_flat_hex() {
let f = vec![0x03, 0x00, 0xff];
let json = serde_json::to_string(&Flags { f: f.clone() }).unwrap();
assert!(json.contains("\"0300ff\""), "{json}");
assert_eq!(serde_json::from_str::<Flags>(&json).unwrap().f, f);
}
#[test]
fn wasm_round_trips_as_base64() {
let w = b"\0asm\x01\0\0\0".to_vec();
let json = serde_json::to_string(&Wasm { w: w.clone() }).unwrap();
assert!(!json.contains("\\u"), "base64 is plain ascii: {json}");
assert_eq!(serde_json::from_str::<Wasm>(&json).unwrap().w, w);
}
#[test]
fn versioned_flattens_the_version_field() {
let v = Versioned {
version: 1,
inner: Flags { f: vec![0x01] },
};
let json = serde_json::to_string(&v).unwrap();
assert_eq!(json, r#"{"version":1,"f":"01"}"#);
}
#[test]
fn readable_json_inlines_small_arrays_and_breaks_grids() {
let value = serde_json::json!({
"quad": [33, 3, 5, 0],
"rows": ["aaaa", "bbbb"],
});
let s = to_readable_json(&value).unwrap();
assert!(s.contains("\"quad\": [33,3,5,0]"), "{s}");
assert!(s.contains("{\n"), "{s}");
}
}