prosaic-core 0.6.2

General-purpose natural language generation from structured data
Documentation
use prosaic_core::{Context, HasProsaicSchema, IntoContext, Value, ValueType, schema_lookup};
use prosaic_derive::IntoContext;

#[derive(IntoContext)]
struct RenameEvent {
    entity_type: String,
    old_name: String,
    new_name: String,
    consumer_count: i64,
    consumers: Vec<String>,
}

#[test]
fn derive_into_context_basic() {
    let event = RenameEvent {
        entity_type: "class".to_string(),
        old_name: "Foo".to_string(),
        new_name: "Foobar".to_string(),
        consumer_count: 6,
        consumers: vec!["Baz".to_string(), "Qux".to_string()],
    };

    let ctx: Context = event.into_context();

    assert_eq!(
        ctx.get("entity_type"),
        Some(&Value::String("class".to_string()))
    );
    assert_eq!(ctx.get("old_name"), Some(&Value::String("Foo".to_string())));
    assert_eq!(
        ctx.get("new_name"),
        Some(&Value::String("Foobar".to_string()))
    );
    assert_eq!(ctx.get("consumer_count"), Some(&Value::Number(6)));
    assert_eq!(
        ctx.get("consumers"),
        Some(&Value::List(vec!["Baz".to_string(), "Qux".to_string()]))
    );
}

#[derive(IntoContext)]
struct OptionalEvent {
    name: String,
    description: Option<String>,
    count: Option<i64>,
}

#[test]
fn derive_with_option_some() {
    let event = OptionalEvent {
        name: "test".to_string(),
        description: Some("a description".to_string()),
        count: Some(42),
    };

    let ctx = event.into_context();

    assert_eq!(ctx.get("name"), Some(&Value::String("test".to_string())));
    assert_eq!(
        ctx.get("description"),
        Some(&Value::String("a description".to_string()))
    );
    assert_eq!(ctx.get("count"), Some(&Value::Number(42)));
}

#[test]
fn derive_with_option_none() {
    let event = OptionalEvent {
        name: "test".to_string(),
        description: None,
        count: None,
    };

    let ctx = event.into_context();

    assert_eq!(ctx.get("name"), Some(&Value::String("test".to_string())));
    assert_eq!(ctx.get("description"), None);
    assert_eq!(ctx.get("count"), None);
}

#[derive(IntoContext)]
struct NumericTypes {
    a: u32,
    b: usize,
    c: i32,
}

#[test]
fn derive_numeric_types() {
    let event = NumericTypes {
        a: 10,
        b: 20,
        c: -5,
    };
    let ctx = event.into_context();

    assert_eq!(ctx.get("a"), Some(&Value::Number(10)));
    assert_eq!(ctx.get("b"), Some(&Value::Number(20)));
    assert_eq!(ctx.get("c"), Some(&Value::Number(-5)));
}

#[derive(IntoContext)]
struct WideNumerics {
    big_u64: u64,
    big_usize: usize,
}

#[test]
fn derive_u64_saturates_at_i64_max() {
    // Regression: without saturation, u64::MAX silently becomes -1 via `as i64`.
    let event = WideNumerics {
        big_u64: u64::MAX,
        big_usize: 7,
    };
    let ctx = event.into_context();
    assert_eq!(ctx.get("big_u64"), Some(&Value::Number(i64::MAX)));
    assert_eq!(ctx.get("big_usize"), Some(&Value::Number(7)));
}

#[test]
fn derive_usize_saturates_at_i64_max_on_64bit() {
    // On 64-bit platforms usize::MAX exceeds i64::MAX and must saturate.
    // On 32-bit it fits exactly and round-trips.
    let event = WideNumerics {
        big_u64: 42,
        big_usize: usize::MAX,
    };
    let ctx = event.into_context();
    let expected = i64::try_from(usize::MAX).unwrap_or(i64::MAX);
    assert_eq!(ctx.get("big_u64"), Some(&Value::Number(42)));
    assert_eq!(ctx.get("big_usize"), Some(&Value::Number(expected)));
}

#[test]
fn derive_u64_in_i64_range_is_exact() {
    let event = WideNumerics {
        big_u64: 1_000_000_u64,
        big_usize: 2_000_000_usize,
    };
    let ctx = event.into_context();
    assert_eq!(ctx.get("big_u64"), Some(&Value::Number(1_000_000)));
    assert_eq!(ctx.get("big_usize"), Some(&Value::Number(2_000_000)));
}

// ── HasProsaicSchema derive tests ──────────────────────────────────────────────

#[derive(IntoContext)]
#[allow(dead_code)]
struct Doc {
    name: String,
    count: i64,
    tags: Vec<String>,
}

#[test]
fn derived_schema_matches_fields() {
    let schema = <Doc as HasProsaicSchema>::PROSAIC_SCHEMA;
    assert_eq!(schema.len(), 3);
    assert_eq!(schema_lookup(schema, "name"), Some(ValueType::String));
    assert_eq!(schema_lookup(schema, "count"), Some(ValueType::Number));
    assert_eq!(schema_lookup(schema, "tags"), Some(ValueType::List));
}

#[derive(IntoContext)]
#[allow(dead_code)]
struct Sizes {
    big: u64,
    arch: usize,
    small: u8,
}

#[test]
fn derived_schema_maps_wide_numerics_to_number() {
    let schema = <Sizes as HasProsaicSchema>::PROSAIC_SCHEMA;
    assert_eq!(schema_lookup(schema, "big"), Some(ValueType::Number));
    assert_eq!(schema_lookup(schema, "arch"), Some(ValueType::Number));
    assert_eq!(schema_lookup(schema, "small"), Some(ValueType::Number));
}

#[derive(IntoContext)]
#[allow(dead_code)]
struct Borrowed<'a> {
    label: &'a str,
}

#[test]
fn derived_schema_handles_str_reference() {
    let schema = <Borrowed as HasProsaicSchema>::PROSAIC_SCHEMA;
    assert_eq!(schema_lookup(schema, "label"), Some(ValueType::String));
}

#[derive(IntoContext)]
#[allow(dead_code)]
struct BoolFields {
    active: bool,
    maybe: Option<bool>,
}

#[test]
fn derived_schema_maps_bool_to_number() {
    let schema = <BoolFields as HasProsaicSchema>::PROSAIC_SCHEMA;
    assert_eq!(schema_lookup(schema, "active"), Some(ValueType::Number));
    assert_eq!(schema_lookup(schema, "maybe"), Some(ValueType::Number));
}

#[derive(IntoContext)]
#[allow(dead_code)]
struct BoolValues {
    active: bool,
    maybe_true: Option<bool>,
    maybe_none: Option<bool>,
}

#[test]
fn derive_bool_into_context() {
    let event = BoolValues {
        active: true,
        maybe_true: Some(false),
        maybe_none: None,
    };
    let ctx = IntoContext::into_context(event);
    assert_eq!(ctx.get("active"), Some(&Value::Number(1)));
    assert_eq!(ctx.get("maybe_true"), Some(&Value::Number(0)));
    assert_eq!(ctx.get("maybe_none"), None); // Option::None omits the key entirely
}

#[derive(IntoContext)]
#[allow(dead_code)]
struct WithOptVec {
    tags: Option<Vec<String>>,
}

#[test]
fn derived_schema_handles_option_vec_string() {
    let schema = <WithOptVec as HasProsaicSchema>::PROSAIC_SCHEMA;
    assert_eq!(schema_lookup(schema, "tags"), Some(ValueType::List));
}

#[test]
fn derive_option_vec_string_into_context_round_trips() {
    let event = WithOptVec {
        tags: Some(vec!["a".into(), "b".into()]),
    };
    let ctx = IntoContext::into_context(event);
    assert_eq!(
        ctx.get("tags"),
        Some(&Value::List(vec!["a".into(), "b".into()])),
    );

    let none_event = WithOptVec { tags: None };
    let ctx = IntoContext::into_context(none_event);
    assert!(ctx.get("tags").is_none());
}