cbor-ld 0.1.0

CBOR-LD 1.0 processor built on cbor2 with semantic compression, JSON-LD context processing, type tables and deterministic CBOR output.
Documentation
use cbor2::Value;

use crate::constants::{SECURITY_MULTIBASE, XSD_DATETIME};
use crate::{
    DecodeOptions, EncodeOptions, Error, TypeTable, decode, decode_with_loader, encode,
    encode_with_loader,
};

fn text(value: &str) -> Value {
    Value::Text(value.to_owned())
}

fn map(entries: impl IntoIterator<Item = (Value, Value)>) -> Value {
    Value::Map(entries.into_iter().collect())
}

fn array(items: impl IntoIterator<Item = Value>) -> Value {
    Value::Array(items.into_iter().collect())
}

fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|b| format!("{b:02x}")).collect()
}

fn assert_semantic_eq(left: &Value, right: &Value) {
    let left = cbor2::to_canonical_vec(left).unwrap();
    let right = cbor2::to_canonical_vec(right).unwrap();
    assert_eq!(left, right);
}

#[test]
fn encodes_empty_document_without_compression() {
    let bytes = encode(&map([]), EncodeOptions::uncompressed()).unwrap();
    assert_eq!(hex(&bytes), "d9cb1d8200a0");
}

#[test]
fn encodes_empty_document_with_default_table() {
    let bytes = encode(
        &map([]),
        EncodeOptions {
            registry_entry_id: 1,
            type_table: None,
            canonical: true,
        },
    )
    .unwrap();
    assert_eq!(hex(&bytes), "d9cb1d8201a0");
}

#[test]
fn encodes_multibyte_registry_id() {
    let table = TypeTable::new();
    let bytes = encode(&map([]), EncodeOptions::compressed(128, &table)).unwrap();
    assert_eq!(hex(&bytes), "d9cb1d821880a0");
}

#[test]
fn rejects_missing_type_table() {
    let error = encode(
        &map([]),
        EncodeOptions {
            registry_entry_id: 2,
            type_table: None,
            canonical: true,
        },
    )
    .unwrap_err();
    assert_eq!(error, Error::NoTypeTable(2));
}

#[test]
fn round_trips_remote_context_and_multibase_table_value() {
    let context_url = "urn:foo";
    let context = map([(
        text("@context"),
        map([
            (text("ex"), text("https://example.com/")),
            (
                text("foo"),
                map([
                    (text("@id"), text("ex:foo")),
                    (text("@type"), text(SECURITY_MULTIBASE)),
                ]),
            ),
        ]),
    )]);
    let document = map([
        (text("@context"), text(context_url)),
        (text("foo"), text("MAQID")),
    ]);

    let mut table = TypeTable::with_common_tables();
    table.insert("context", context_url, 0x8000);
    table.insert(SECURITY_MULTIBASE, "MAQID", 0x8001);

    let loader = |url: &str| {
        if url == context_url {
            Ok(context.clone())
        } else {
            Err(Error::DocumentLoader(url.to_owned()))
        }
    };
    let bytes =
        encode_with_loader(&document, EncodeOptions::compressed(20, &table), loader).unwrap();

    let decoded = decode_with_loader(
        &bytes,
        DecodeOptions {
            type_table: Some(&table),
        },
        |url| {
            if url == context_url {
                Ok(context.clone())
            } else {
                Err(Error::DocumentLoader(url.to_owned()))
            }
        },
    )
    .unwrap();
    assert_semantic_eq(&decoded, &document);
}

#[test]
fn round_trips_inline_context_url_and_datetime_codecs() {
    let document = map([
        (
            text("@context"),
            map([
                (text("ex"), text("https://example.com/")),
                (
                    text("homepage"),
                    map([
                        (text("@id"), text("ex:homepage")),
                        (text("@type"), text("@id")),
                    ]),
                ),
                (
                    text("created"),
                    map([
                        (text("@id"), text("ex:created")),
                        (text("@type"), text(XSD_DATETIME)),
                    ]),
                ),
            ]),
        ),
        (text("homepage"), text("https://example.com/alice")),
        (text("created"), text("2021-04-09T20:38:55Z")),
    ]);
    let table = TypeTable::new();
    let bytes = encode(&document, EncodeOptions::compressed(1, &table)).unwrap();
    let decoded = decode(&bytes, DecodeOptions { type_table: None }).unwrap();
    assert_semantic_eq(&decoded, &document);
}

#[test]
fn round_trips_data_url_uuid_and_did_key() {
    let document = map([
        (
            text("@context"),
            map([
                (text("ex"), text("https://example.com/")),
                (text("id"), map([(text("@id"), text("@id"))])),
                (
                    text("image"),
                    map([
                        (text("@id"), text("ex:image")),
                        (text("@type"), text("@id")),
                    ]),
                ),
                (
                    text("uuid"),
                    map([(text("@id"), text("ex:uuid")), (text("@type"), text("@id"))]),
                ),
            ]),
        ),
        (text("id"), text("did:key:z6MkiTBz")),
        (text("image"), text("data:text/plain;base64,AQID")),
        (
            text("uuid"),
            text("urn:uuid:550e8400-e29b-41d4-a716-446655440000"),
        ),
    ]);
    let table = TypeTable::new();
    let bytes = encode(&document, EncodeOptions::compressed(1, &table)).unwrap();
    let decoded = decode(&bytes, DecodeOptions::default()).unwrap();
    assert_semantic_eq(&decoded, &document);
}

#[test]
fn decodes_uncompressed_payload() {
    let document = map([(text("hello"), text("world"))]);
    let bytes = encode(&document, EncodeOptions::uncompressed()).unwrap();
    let decoded = decode(&bytes, DecodeOptions::default()).unwrap();
    assert_semantic_eq(&decoded, &document);
}

#[test]
fn top_level_array_round_trips() {
    let context = map([(text("name"), text("https://schema.org/name"))]);
    let document = array([
        map([
            (text("@context"), context.clone()),
            (text("name"), text("Alice")),
        ]),
        map([(text("@context"), context), (text("name"), text("Bob"))]),
    ]);
    let table = TypeTable::new();
    let bytes = encode(&document, EncodeOptions::compressed(1, &table)).unwrap();
    let decoded = decode(&bytes, DecodeOptions::default()).unwrap();
    assert_eq!(decoded, document);
}