#[cfg(not(feature = "std"))]
use alloc::string::{String, ToString};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::collections::HashMap;
use crate::agreement::AgreementFeatures;
use prosaic_common::ValueType;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Value {
String(String),
Number(i64),
List(Vec<String>),
Entity {
name: String,
#[cfg_attr(feature = "serde", serde(default))]
features: AgreementFeatures,
},
}
impl Value {
pub fn as_display(&self) -> String {
match self {
Value::String(s) => s.clone(),
Value::Number(n) => {
let mut buf = itoa::Buffer::new();
buf.format(*n).to_string()
}
Value::List(items) => items.join(", "),
Value::Entity { name, .. } => name.clone(),
}
}
pub fn as_number(&self) -> Option<i64> {
match self {
Value::Number(n) => Some(*n),
_ => None,
}
}
pub fn as_list(&self) -> Option<&[String]> {
match self {
Value::List(items) => Some(items),
_ => None,
}
}
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Context {
values: HashMap<String, Value>,
}
impl Context {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, key: impl Into<String>, value: Value) {
self.values.insert(key.into(), value);
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.values.get(key)
}
pub fn keys(&self) -> impl Iterator<Item = &String> {
self.values.keys()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
self.values.iter().map(|(k, v)| (k.as_str(), v))
}
}
pub trait IntoValue {
fn into_value(self) -> Value;
}
impl IntoValue for &str {
fn into_value(self) -> Value {
Value::String(self.to_string())
}
}
impl IntoValue for String {
fn into_value(self) -> Value {
Value::String(self)
}
}
impl IntoValue for &String {
fn into_value(self) -> Value {
Value::String(self.clone())
}
}
macro_rules! impl_into_value_int {
($($t:ty),*) => {
$(impl IntoValue for $t {
fn into_value(self) -> Value { Value::Number(self as i64) }
})*
};
}
impl_into_value_int!(i8, i16, i32, i64, isize, u8, u16, u32);
impl IntoValue for u64 {
fn into_value(self) -> Value {
Value::Number(i64::try_from(self).unwrap_or(i64::MAX))
}
}
impl IntoValue for usize {
fn into_value(self) -> Value {
Value::Number(i64::try_from(self).unwrap_or(i64::MAX))
}
}
impl IntoValue for bool {
fn into_value(self) -> Value {
Value::Number(if self { 1 } else { 0 })
}
}
impl IntoValue for Value {
fn into_value(self) -> Value {
self
}
}
impl IntoValue for Vec<String> {
fn into_value(self) -> Value {
Value::List(self)
}
}
impl IntoValue for Vec<&str> {
fn into_value(self) -> Value {
Value::List(self.iter().map(|s| s.to_string()).collect())
}
}
impl<const N: usize> IntoValue for [&str; N] {
fn into_value(self) -> Value {
Value::List(self.iter().map(|s| s.to_string()).collect())
}
}
impl<const N: usize> IntoValue for [String; N] {
fn into_value(self) -> Value {
Value::List(self.to_vec())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntityValue {
name: String,
features: crate::agreement::AgreementFeatures,
}
impl EntityValue {
pub fn masc(mut self) -> Self {
self.features.gender = crate::agreement::Gender::Masc;
self
}
pub fn fem(mut self) -> Self {
self.features.gender = crate::agreement::Gender::Fem;
self
}
pub fn neut(mut self) -> Self {
self.features.gender = crate::agreement::Gender::Neut;
self
}
pub fn common(mut self) -> Self {
self.features.gender = crate::agreement::Gender::Common;
self
}
pub fn sing(mut self) -> Self {
self.features.number = crate::agreement::Number::Singular;
self
}
pub fn plur(mut self) -> Self {
self.features.number = crate::agreement::Number::Plural;
self
}
pub fn dual(mut self) -> Self {
self.features.number = crate::agreement::Number::Dual;
self
}
pub fn defined(mut self) -> Self {
self.features.definiteness = crate::agreement::Definiteness::Definite;
self
}
pub fn indef(mut self) -> Self {
self.features.definiteness = crate::agreement::Definiteness::Indefinite;
self
}
pub fn animate(mut self) -> Self {
self.features.animacy = crate::agreement::Animacy::Animate;
self
}
pub fn inanimate(mut self) -> Self {
self.features.animacy = crate::agreement::Animacy::Inanimate;
self
}
pub fn case(mut self, c: crate::agreement::Case) -> Self {
self.features.case = c;
self
}
pub fn person(mut self, p: crate::agreement::AgreementPerson) -> Self {
self.features.person = p;
self
}
pub fn with_features(mut self, f: crate::agreement::AgreementFeatures) -> Self {
self.features = f;
self
}
pub fn build(self) -> Value {
Value::Entity {
name: self.name,
features: self.features,
}
}
}
impl IntoValue for EntityValue {
fn into_value(self) -> Value {
self.build()
}
}
pub fn entity(name: impl Into<String>) -> EntityValue {
EntityValue {
name: name.into(),
features: crate::agreement::AgreementFeatures::default(),
}
}
#[macro_export]
macro_rules! ctx {
() => { $crate::Context::new() };
( $( $key:ident : $value:expr ),* $(,)? ) => {{
let mut c = $crate::Context::new();
$(
$crate::Context::insert(&mut c, stringify!($key), $crate::IntoValue::into_value($value));
)*
c
}};
}
pub trait IntoContext {
fn into_context(self) -> Context;
}
impl IntoContext for Context {
fn into_context(self) -> Context {
self
}
}
impl IntoContext for &Context {
fn into_context(self) -> Context {
self.clone()
}
}
pub trait HasProsaicSchema {
const PROSAIC_SCHEMA: &'static [(&'static str, ValueType)];
}
#[cfg(test)]
mod into_value_tests {
use super::*;
#[test]
fn str_becomes_value_string() {
let v: Value = "hello".into_value();
assert_eq!(v, Value::String("hello".into()));
}
#[test]
fn owned_string_becomes_value_string_without_clone() {
let s = String::from("hello");
let v: Value = s.into_value();
assert_eq!(v, Value::String("hello".into()));
}
#[test]
fn borrowed_string_becomes_value_string() {
let s = String::from("hello");
let v: Value = (&s).into_value();
assert_eq!(v, Value::String("hello".into()));
}
#[test]
fn i64_becomes_value_number() {
let v: Value = 42_i64.into_value();
assert_eq!(v, Value::Number(42));
}
#[test]
fn i32_becomes_value_number() {
let v: Value = 42_i32.into_value();
assert_eq!(v, Value::Number(42));
}
#[test]
fn usize_becomes_value_number() {
let v: Value = 7_usize.into_value();
assert_eq!(v, Value::Number(7));
}
#[test]
fn bool_becomes_number_zero_or_one() {
assert_eq!(true.into_value(), Value::Number(1));
assert_eq!(false.into_value(), Value::Number(0));
}
#[test]
fn u64_saturates_at_i64_max() {
let v: Value = u64::MAX.into_value();
assert_eq!(v, Value::Number(i64::MAX));
}
#[test]
fn u64_in_range_is_exact() {
let v: Value = 1_234_567_u64.into_value();
assert_eq!(v, Value::Number(1_234_567));
}
#[test]
fn usize_saturates_at_i64_max_on_64bit() {
let v: Value = usize::MAX.into_value();
let expected = i64::try_from(usize::MAX).unwrap_or(i64::MAX);
assert_eq!(v, Value::Number(expected));
}
#[test]
fn usize_in_range_is_exact() {
let v: Value = 42_usize.into_value();
assert_eq!(v, Value::Number(42));
}
#[test]
fn vec_of_str_becomes_value_list() {
let v: Value = vec!["a", "b"].into_value();
assert_eq!(v, Value::List(vec!["a".into(), "b".into()]));
}
#[test]
fn array_of_str_becomes_value_list() {
let v: Value = ["a", "b"].into_value();
assert_eq!(v, Value::List(vec!["a".into(), "b".into()]));
}
#[test]
fn value_passes_through_identity() {
let v = Value::Number(99);
assert_eq!(v.clone().into_value(), v);
}
}
#[cfg(test)]
mod ctx_macro_tests {
use crate::{Context, Value};
#[test]
fn empty_ctx_is_empty() {
let c: Context = ctx! {};
assert_eq!(c.get("anything"), None);
}
#[test]
fn single_slot() {
let c = ctx! { name: "Foo" };
assert_eq!(c.get("name"), Some(&Value::String("Foo".into())));
}
#[test]
fn multiple_slots_mixed_types() {
let c = ctx! {
name: "Foo",
count: 3,
flag: true,
};
assert_eq!(c.get("name"), Some(&Value::String("Foo".into())));
assert_eq!(c.get("count"), Some(&Value::Number(3)));
assert_eq!(c.get("flag"), Some(&Value::Number(1)));
}
#[test]
fn list_slot_from_array() {
let c = ctx! { items: ["a", "b", "c"] };
assert_eq!(
c.get("items"),
Some(&Value::List(vec!["a".into(), "b".into(), "c".into()]))
);
}
#[test]
fn trailing_comma_allowed() {
let c = ctx! { a: 1, b: 2, };
assert_eq!(c.get("a"), Some(&Value::Number(1)));
assert_eq!(c.get("b"), Some(&Value::Number(2)));
}
#[test]
fn expression_values_are_evaluated() {
let s = String::from("dynamic");
let c = ctx! { name: s };
assert_eq!(c.get("name"), Some(&Value::String("dynamic".into())));
}
#[test]
fn value_literal_passes_through() {
let c = ctx! { x: Value::Number(7) };
assert_eq!(c.get("x"), Some(&Value::Number(7)));
}
}
#[cfg(test)]
mod entity_value_tests {
use super::*;
use crate::agreement::{AgreementFeatures, Gender, Number};
#[test]
fn entity_display_is_name() {
let v = Value::Entity {
name: "UserService".into(),
features: AgreementFeatures::default(),
};
assert_eq!(v.as_display(), "UserService");
}
#[test]
fn entity_as_list_is_none() {
let v = Value::Entity {
name: "X".into(),
features: AgreementFeatures::default(),
};
assert!(v.as_list().is_none());
}
#[test]
fn entity_as_number_is_none() {
let v = Value::Entity {
name: "Service".into(),
features: AgreementFeatures::default(),
};
assert!(v.as_number().is_none());
}
#[test]
fn entity_with_features_round_trips_via_equality() {
let features = AgreementFeatures::new()
.with_gender(Gender::Fem)
.with_number(Number::Singular);
let v1 = Value::Entity {
name: "Alice".into(),
features,
};
let v2 = v1.clone();
assert_eq!(v1, v2);
}
#[test]
fn entity_display_ignores_features() {
let v_plain = Value::Entity {
name: "Alice".into(),
features: AgreementFeatures::default(),
};
let v_with_features = Value::Entity {
name: "Alice".into(),
features: AgreementFeatures::new()
.with_gender(Gender::Fem)
.with_number(Number::Singular),
};
assert_eq!(v_plain.as_display(), v_with_features.as_display());
}
}
#[cfg(test)]
mod entity_builder_tests {
use super::*;
use crate::agreement::{AgreementFeatures, Animacy, Case, Definiteness, Gender, Number};
#[test]
fn entity_helper_default_features() {
let ev = entity("UserService");
let v = ev.into_value();
match v {
Value::Entity { name, features } => {
assert_eq!(name, "UserService");
assert_eq!(features, AgreementFeatures::default());
}
_ => panic!("expected Value::Entity"),
}
}
#[test]
fn entity_builder_chain_sets_features() {
let v = entity("Alice")
.fem()
.sing()
.defined()
.animate()
.into_value();
match v {
Value::Entity { name, features } => {
assert_eq!(name, "Alice");
assert_eq!(features.gender, Gender::Fem);
assert_eq!(features.number, Number::Singular);
assert_eq!(features.definiteness, Definiteness::Definite);
assert_eq!(features.animacy, Animacy::Animate);
}
_ => panic!("expected Value::Entity"),
}
}
#[test]
fn entity_builder_all_gender_shortcuts() {
assert_eq!(
entity("x").masc().into_value(),
Value::Entity {
name: "x".into(),
features: AgreementFeatures::new().with_gender(Gender::Masc),
}
);
assert_eq!(
entity("x").fem().into_value(),
Value::Entity {
name: "x".into(),
features: AgreementFeatures::new().with_gender(Gender::Fem),
}
);
assert_eq!(
entity("x").neut().into_value(),
Value::Entity {
name: "x".into(),
features: AgreementFeatures::new().with_gender(Gender::Neut),
}
);
assert_eq!(
entity("x").common().into_value(),
Value::Entity {
name: "x".into(),
features: AgreementFeatures::new().with_gender(Gender::Common),
}
);
}
#[test]
fn entity_builder_all_number_shortcuts() {
assert_eq!(entity("x").plur().features.number, Number::Plural);
assert_eq!(entity("x").dual().features.number, Number::Dual);
assert_eq!(entity("x").sing().features.number, Number::Singular);
}
#[test]
fn entity_builder_definiteness_shortcuts() {
assert_eq!(
entity("x").defined().features.definiteness,
Definiteness::Definite
);
assert_eq!(
entity("x").indef().features.definiteness,
Definiteness::Indefinite
);
}
#[test]
fn entity_builder_animacy_shortcuts() {
assert_eq!(entity("x").animate().features.animacy, Animacy::Animate);
assert_eq!(entity("x").inanimate().features.animacy, Animacy::Inanimate);
}
#[test]
fn entity_builder_case_method() {
assert_eq!(
entity("x").case(Case::Genitive).features.case,
Case::Genitive
);
}
#[test]
fn entity_builder_with_features_override() {
let full = AgreementFeatures::new()
.with_gender(Gender::Fem)
.with_number(Number::Plural);
let v = entity("items").with_features(full).into_value();
match v {
Value::Entity { features, .. } => {
assert_eq!(features.gender, Gender::Fem);
assert_eq!(features.number, Number::Plural);
}
_ => panic!("expected Value::Entity"),
}
}
#[test]
fn ctx_macro_accepts_entity_value() {
let c = ctx! {
user: entity("Alice").fem().sing(),
count: 3,
};
match c.get("user").unwrap() {
Value::Entity { name, features } => {
assert_eq!(name, "Alice");
assert_eq!(features.gender, Gender::Fem);
}
_ => panic!("expected Value::Entity"),
}
assert_eq!(c.get("count"), Some(&Value::Number(3)));
}
#[test]
fn entity_build_and_into_value_are_equivalent() {
let ev1 = entity("TestService").fem();
let ev2 = ev1.clone();
assert_eq!(ev1.build(), ev2.into_value());
}
}
#[cfg(test)]
mod has_schema_tests {
use super::*;
use prosaic_common::{ValueType, schema_lookup};
struct Manual;
impl HasProsaicSchema for Manual {
const PROSAIC_SCHEMA: &'static [(&'static str, ValueType)] =
&[("count", ValueType::Number), ("name", ValueType::String)];
}
#[test]
fn manual_impl_exposes_schema() {
assert_eq!(Manual::PROSAIC_SCHEMA.len(), 2);
}
#[test]
fn schema_is_const_queryable() {
const T: Option<ValueType> =
schema_lookup(<Manual as HasProsaicSchema>::PROSAIC_SCHEMA, "count");
assert_eq!(T, Some(ValueType::Number));
}
}