sark 0.8.0

Simple Asynchronous Rust webKit - Server
Documentation
use o3::buffer::Shared;
use sark::json::{JsonDecode, JsonEncode, Scratch};
use sark_core::http::LocalFrameBytes;

#[sark_gen::json(ordered)]
struct Message {
    message: LocalFrameBytes,
}

#[allow(non_snake_case)]
#[sark_gen::json(ordered)]
struct World {
    id: u64,
    randomNumber: u64,
}

#[sark_gen::json(ordered)]
struct Fortune {
    id: u64,
    message: LocalFrameBytes,
}

#[sark_gen::json(ordered)]
struct Rating {
    score: u64,
    count: u64,
}

#[sark_gen::json(ordered)]
struct Item {
    id: u64,
    name: LocalFrameBytes,
    category: LocalFrameBytes,
    price: u64,
    quantity: u64,
    active: bool,
    #[field(seq)]
    tags: Vec<LocalFrameBytes>,
    #[field(nested)]
    rating: Rating,
    total: u64,
}

#[sark_gen::json(ordered)]
struct ItemsResponse {
    #[field(seq, nested)]
    items: Vec<Item>,
    count: u64,
}

#[sark_gen::json]
struct Node {
    #[field(seq, nested)]
    children: Vec<Node>,
}

fn nested_node_json(depth: usize) -> String {
    let mut s = String::with_capacity(depth * 14 + 2);
    for _ in 0..depth {
        s.push_str("{\"children\":[");
    }
    for _ in 0..depth {
        s.push_str("]}");
    }
    s
}

#[test]
fn recursive_typed_shallow_decodes() {
    let json = nested_node_json(16);
    let decoded = Node::decode_json_borrowed(json.as_bytes()).expect("shallow recursive decodes");
    let mut node = &decoded;
    let mut levels = 1;
    while let Some(child) = node.children.first() {
        node = child;
        levels += 1;
    }
    assert_eq!(levels, 16);
}

#[test]
fn recursive_typed_deep_errors_without_overflow() {
    let json = nested_node_json(5_000);
    let result = Node::decode_json_borrowed(json.as_bytes());
    assert!(
        result.is_err(),
        "deeply nested typed JSON must yield a decode error, not a stack overflow"
    );
}

fn lfb(value: &'static [u8]) -> LocalFrameBytes {
    LocalFrameBytes::from_shared(Shared::from_static(value))
}

fn encode<T: JsonEncode>(value: &T) -> Vec<u8> {
    let out = value.encode_json();
    assert_eq!(
        value.json_len(),
        out.as_ref().len(),
        "json_len must equal encoded length"
    );
    out.as_ref().to_vec()
}

fn sample_item() -> Item {
    Item {
        id: 1,
        name: lfb(b"Alpha"),
        category: lfb(b"electronics"),
        price: 328,
        quantity: 15,
        active: true,
        tags: vec![lfb(b"fast"), lfb(b"new")],
        rating: Rating {
            score: 48,
            count: 127,
        },
        total: 14760,
    }
}

#[test]
fn nested_object_encodes() {
    let rating = Rating {
        score: 48,
        count: 127,
    };
    assert_eq!(encode(&rating), br#"{"score":48,"count":127}"#);
}

#[test]
fn item_with_array_and_nested_encodes() {
    let bytes = encode(&sample_item());
    assert_eq!(
        bytes,
        br#"{"id":1,"name":"Alpha","category":"electronics","price":328,"quantity":15,"active":true,"tags":["fast","new"],"rating":{"score":48,"count":127},"total":14760}"#
    );
}

#[test]
fn array_of_nested_objects_encodes() {
    let response = ItemsResponse {
        items: vec![sample_item(), sample_item()],
        count: 2,
    };
    let bytes = encode(&response);
    let expected_item = br#"{"id":1,"name":"Alpha","category":"electronics","price":328,"quantity":15,"active":true,"tags":["fast","new"],"rating":{"score":48,"count":127},"total":14760}"#;
    let mut expected = Vec::new();
    expected.extend_from_slice(b"{\"items\":[");
    expected.extend_from_slice(expected_item);
    expected.extend_from_slice(b",");
    expected.extend_from_slice(expected_item);
    expected.extend_from_slice(b"],\"count\":2}");
    assert_eq!(bytes, expected);
}

#[test]
fn empty_arrays_encode_as_brackets() {
    let mut item = sample_item();
    item.tags = Vec::new();
    let bytes = encode(&item);
    assert!(
        std::str::from_utf8(&bytes)
            .unwrap()
            .contains(r#""tags":[],"#),
        "empty string array must encode as []"
    );

    let response = ItemsResponse {
        items: Vec::new(),
        count: 0,
    };
    assert_eq!(encode(&response), br#"{"items":[],"count":0}"#);
}

#[test]
fn single_element_array_has_no_comma() {
    let mut item = sample_item();
    item.tags = vec![lfb(b"solo")];
    let bytes = encode(&item);
    assert!(
        std::str::from_utf8(&bytes)
            .unwrap()
            .contains(r#""tags":["solo"],"#)
    );
}

#[test]
fn array_elements_are_escaped() {
    let mut item = sample_item();
    item.tags = vec![lfb(b"a\"b"), lfb(b"c\nd")];
    let bytes = encode(&item);
    assert!(
        std::str::from_utf8(&bytes)
            .unwrap()
            .contains(r#""tags":["a\"b","c\nd"],"#)
    );
}

#[test]
fn tfb_message_shape() {
    let value = Message {
        message: lfb(b"Hello, World!"),
    };
    assert_eq!(encode(&value), br#"{"message":"Hello, World!"}"#);
}

#[test]
fn world_shape() {
    let value = World {
        id: 4242,
        randomNumber: 88,
    };
    assert_eq!(encode(&value), br#"{"id":4242,"randomNumber":88}"#);
}

#[test]
fn fortune_shape_with_escapes() {
    let value = Fortune {
        id: 11,
        message: lfb(b"<script>alert(\"x\")</script>\n\tend"),
    };
    let bytes = encode(&value);
    assert_eq!(
        bytes,
        &br#"{"id":11,"message":"<script>alert(\"x\")</script>\n\tend"}"#[..]
    );
}

#[test]
fn control_chars_six_byte_escape_identity() {
    let value = Fortune {
        id: 0,
        message: lfb(&[0x00, b'a', 0x1f, b'b']),
    };
    let bytes = encode(&value);
    let mut expected = Vec::new();
    expected.extend_from_slice(br#"{"id":0,"message":""#);
    expected.extend_from_slice(b"\\u0000a\\u001fb");
    expected.extend_from_slice(br#""}"#);
    assert_eq!(bytes, expected);
}

#[test]
fn scratch_reuse_yields_identical_bytes() {
    let scratch: Scratch<Item> = Scratch::new();
    let mut prev: Option<Vec<u8>> = None;
    for _ in 0..5 {
        let mut items = scratch.take();
        items.push(sample_item());
        items.push(sample_item());
        let response = ItemsResponse { items, count: 2 };
        let bytes = encode(&response);
        if let Some(ref expected) = prev {
            assert_eq!(
                &bytes, expected,
                "scratch reuse must produce identical bytes"
            );
        }
        scratch.give(response.items);
        prev = Some(bytes);
    }
}

#[test]
fn round_trips_through_decode() {
    let response = ItemsResponse {
        items: vec![sample_item(), sample_item()],
        count: 2,
    };
    let encoded = response.encode_json();
    let decoded = ItemsResponse::decode_json(encoded.freeze()).expect("decode");
    assert_eq!(decoded.count, 2);
    assert_eq!(decoded.items.len(), 2);
    let item = &decoded.items[0];
    assert_eq!(item.id, 1);
    assert_eq!(item.name.as_bytes(), b"Alpha");
    assert_eq!(item.category.as_bytes(), b"electronics");
    assert_eq!(item.price, 328);
    assert!(item.active);
    assert_eq!(item.tags.len(), 2);
    assert_eq!(item.tags[0].as_bytes(), b"fast");
    assert_eq!(item.tags[1].as_bytes(), b"new");
    assert_eq!(item.rating.score, 48);
    assert_eq!(item.rating.count, 127);
    assert_eq!(item.total, 14760);
}