1#[cfg(not(feature = "std"))]
2use alloc::string::{String, ToString};
3#[cfg(not(feature = "std"))]
4use alloc::vec::Vec;
5
6use crate::collections::HashMap;
7
8use crate::agreement::AgreementFeatures;
9use prosaic_common::ValueType;
10
11#[derive(Debug, Clone, PartialEq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum Value {
20 String(String),
21 Number(i64),
22 List(Vec<String>),
23 Entity {
29 name: String,
30 #[cfg_attr(feature = "serde", serde(default))]
31 features: AgreementFeatures,
32 },
33}
34
35impl Value {
36 pub fn as_display(&self) -> String {
41 match self {
42 Value::String(s) => s.clone(),
43 Value::Number(n) => {
44 let mut buf = itoa::Buffer::new();
45 buf.format(*n).to_string()
46 }
47 Value::List(items) => items.join(", "),
48 Value::Entity { name, .. } => name.clone(),
49 }
50 }
51
52 pub fn as_number(&self) -> Option<i64> {
54 match self {
55 Value::Number(n) => Some(*n),
56 _ => None,
57 }
58 }
59
60 pub fn as_list(&self) -> Option<&[String]> {
64 match self {
65 Value::List(items) => Some(items),
66 _ => None,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Default)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
74#[cfg_attr(feature = "serde", serde(transparent))]
75pub struct Context {
76 values: HashMap<String, Value>,
77}
78
79impl Context {
80 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn insert(&mut self, key: impl Into<String>, value: Value) {
86 self.values.insert(key.into(), value);
87 }
88
89 pub fn get(&self, key: &str) -> Option<&Value> {
91 self.values.get(key)
92 }
93
94 pub fn keys(&self) -> impl Iterator<Item = &String> {
96 self.values.keys()
97 }
98
99 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
101 self.values.iter().map(|(k, v)| (k.as_str(), v))
102 }
103}
104
105pub trait IntoValue {
120 fn into_value(self) -> Value;
121}
122
123impl IntoValue for &str {
124 fn into_value(self) -> Value {
125 Value::String(self.to_string())
126 }
127}
128
129impl IntoValue for String {
130 fn into_value(self) -> Value {
131 Value::String(self)
132 }
133}
134
135impl IntoValue for &String {
136 fn into_value(self) -> Value {
137 Value::String(self.clone())
138 }
139}
140
141macro_rules! impl_into_value_int {
142 ($($t:ty),*) => {
143 $(impl IntoValue for $t {
144 fn into_value(self) -> Value { Value::Number(self as i64) }
145 })*
146 };
147}
148impl_into_value_int!(i8, i16, i32, i64, isize, u8, u16, u32);
149
150impl IntoValue for u64 {
153 fn into_value(self) -> Value {
154 Value::Number(i64::try_from(self).unwrap_or(i64::MAX))
155 }
156}
157
158impl IntoValue for usize {
159 fn into_value(self) -> Value {
160 Value::Number(i64::try_from(self).unwrap_or(i64::MAX))
161 }
162}
163
164impl IntoValue for bool {
165 fn into_value(self) -> Value {
166 Value::Number(if self { 1 } else { 0 })
167 }
168}
169
170impl IntoValue for Value {
171 fn into_value(self) -> Value {
172 self
173 }
174}
175
176impl IntoValue for Vec<String> {
177 fn into_value(self) -> Value {
178 Value::List(self)
179 }
180}
181
182impl IntoValue for Vec<&str> {
183 fn into_value(self) -> Value {
184 Value::List(self.iter().map(|s| s.to_string()).collect())
185 }
186}
187
188impl<const N: usize> IntoValue for [&str; N] {
189 fn into_value(self) -> Value {
190 Value::List(self.iter().map(|s| s.to_string()).collect())
191 }
192}
193
194impl<const N: usize> IntoValue for [String; N] {
195 fn into_value(self) -> Value {
196 Value::List(self.to_vec())
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct EntityValue {
226 name: String,
227 features: crate::agreement::AgreementFeatures,
228}
229
230impl EntityValue {
231 pub fn masc(mut self) -> Self {
235 self.features.gender = crate::agreement::Gender::Masc;
236 self
237 }
238
239 pub fn fem(mut self) -> Self {
241 self.features.gender = crate::agreement::Gender::Fem;
242 self
243 }
244
245 pub fn neut(mut self) -> Self {
247 self.features.gender = crate::agreement::Gender::Neut;
248 self
249 }
250
251 pub fn common(mut self) -> Self {
253 self.features.gender = crate::agreement::Gender::Common;
254 self
255 }
256
257 pub fn sing(mut self) -> Self {
261 self.features.number = crate::agreement::Number::Singular;
262 self
263 }
264
265 pub fn plur(mut self) -> Self {
267 self.features.number = crate::agreement::Number::Plural;
268 self
269 }
270
271 pub fn dual(mut self) -> Self {
273 self.features.number = crate::agreement::Number::Dual;
274 self
275 }
276
277 pub fn defined(mut self) -> Self {
281 self.features.definiteness = crate::agreement::Definiteness::Definite;
282 self
283 }
284
285 pub fn indef(mut self) -> Self {
287 self.features.definiteness = crate::agreement::Definiteness::Indefinite;
288 self
289 }
290
291 pub fn animate(mut self) -> Self {
295 self.features.animacy = crate::agreement::Animacy::Animate;
296 self
297 }
298
299 pub fn inanimate(mut self) -> Self {
301 self.features.animacy = crate::agreement::Animacy::Inanimate;
302 self
303 }
304
305 pub fn case(mut self, c: crate::agreement::Case) -> Self {
309 self.features.case = c;
310 self
311 }
312
313 pub fn person(mut self, p: crate::agreement::AgreementPerson) -> Self {
315 self.features.person = p;
316 self
317 }
318
319 pub fn with_features(mut self, f: crate::agreement::AgreementFeatures) -> Self {
323 self.features = f;
324 self
325 }
326
327 pub fn build(self) -> Value {
331 Value::Entity {
332 name: self.name,
333 features: self.features,
334 }
335 }
336}
337
338impl IntoValue for EntityValue {
339 fn into_value(self) -> Value {
340 self.build()
341 }
342}
343
344pub fn entity(name: impl Into<String>) -> EntityValue {
361 EntityValue {
362 name: name.into(),
363 features: crate::agreement::AgreementFeatures::default(),
364 }
365}
366
367#[macro_export]
389macro_rules! ctx {
390 () => { $crate::Context::new() };
391 ( $( $key:ident : $value:expr ),* $(,)? ) => {{
392 let mut c = $crate::Context::new();
393 $(
394 $crate::Context::insert(&mut c, stringify!($key), $crate::IntoValue::into_value($value));
395 )*
396 c
397 }};
398}
399
400pub trait IntoContext {
402 fn into_context(self) -> Context;
403}
404
405impl IntoContext for Context {
406 fn into_context(self) -> Context {
407 self
408 }
409}
410
411impl IntoContext for &Context {
414 fn into_context(self) -> Context {
415 self.clone()
416 }
417}
418
419pub trait HasProsaicSchema {
431 const PROSAIC_SCHEMA: &'static [(&'static str, ValueType)];
432}
433
434#[cfg(test)]
435mod into_value_tests {
436 use super::*;
437
438 #[test]
439 fn str_becomes_value_string() {
440 let v: Value = "hello".into_value();
441 assert_eq!(v, Value::String("hello".into()));
442 }
443
444 #[test]
445 fn owned_string_becomes_value_string_without_clone() {
446 let s = String::from("hello");
447 let v: Value = s.into_value();
448 assert_eq!(v, Value::String("hello".into()));
449 }
450
451 #[test]
452 fn borrowed_string_becomes_value_string() {
453 let s = String::from("hello");
454 let v: Value = (&s).into_value();
455 assert_eq!(v, Value::String("hello".into()));
456 }
457
458 #[test]
459 fn i64_becomes_value_number() {
460 let v: Value = 42_i64.into_value();
461 assert_eq!(v, Value::Number(42));
462 }
463
464 #[test]
465 fn i32_becomes_value_number() {
466 let v: Value = 42_i32.into_value();
467 assert_eq!(v, Value::Number(42));
468 }
469
470 #[test]
471 fn usize_becomes_value_number() {
472 let v: Value = 7_usize.into_value();
473 assert_eq!(v, Value::Number(7));
474 }
475
476 #[test]
477 fn bool_becomes_number_zero_or_one() {
478 assert_eq!(true.into_value(), Value::Number(1));
479 assert_eq!(false.into_value(), Value::Number(0));
480 }
481
482 #[test]
483 fn u64_saturates_at_i64_max() {
484 let v: Value = u64::MAX.into_value();
486 assert_eq!(v, Value::Number(i64::MAX));
487 }
488
489 #[test]
490 fn u64_in_range_is_exact() {
491 let v: Value = 1_234_567_u64.into_value();
492 assert_eq!(v, Value::Number(1_234_567));
493 }
494
495 #[test]
496 fn usize_saturates_at_i64_max_on_64bit() {
497 let v: Value = usize::MAX.into_value();
500 let expected = i64::try_from(usize::MAX).unwrap_or(i64::MAX);
501 assert_eq!(v, Value::Number(expected));
502 }
503
504 #[test]
505 fn usize_in_range_is_exact() {
506 let v: Value = 42_usize.into_value();
507 assert_eq!(v, Value::Number(42));
508 }
509
510 #[test]
511 fn vec_of_str_becomes_value_list() {
512 let v: Value = vec!["a", "b"].into_value();
513 assert_eq!(v, Value::List(vec!["a".into(), "b".into()]));
514 }
515
516 #[test]
517 fn array_of_str_becomes_value_list() {
518 let v: Value = ["a", "b"].into_value();
519 assert_eq!(v, Value::List(vec!["a".into(), "b".into()]));
520 }
521
522 #[test]
523 fn value_passes_through_identity() {
524 let v = Value::Number(99);
525 assert_eq!(v.clone().into_value(), v);
526 }
527}
528
529#[cfg(test)]
530mod ctx_macro_tests {
531 use crate::{Context, Value};
532
533 #[test]
534 fn empty_ctx_is_empty() {
535 let c: Context = ctx! {};
536 assert_eq!(c.get("anything"), None);
537 }
538
539 #[test]
540 fn single_slot() {
541 let c = ctx! { name: "Foo" };
542 assert_eq!(c.get("name"), Some(&Value::String("Foo".into())));
543 }
544
545 #[test]
546 fn multiple_slots_mixed_types() {
547 let c = ctx! {
548 name: "Foo",
549 count: 3,
550 flag: true,
551 };
552 assert_eq!(c.get("name"), Some(&Value::String("Foo".into())));
553 assert_eq!(c.get("count"), Some(&Value::Number(3)));
554 assert_eq!(c.get("flag"), Some(&Value::Number(1)));
555 }
556
557 #[test]
558 fn list_slot_from_array() {
559 let c = ctx! { items: ["a", "b", "c"] };
560 assert_eq!(
561 c.get("items"),
562 Some(&Value::List(vec!["a".into(), "b".into(), "c".into()]))
563 );
564 }
565
566 #[test]
567 fn trailing_comma_allowed() {
568 let c = ctx! { a: 1, b: 2, };
569 assert_eq!(c.get("a"), Some(&Value::Number(1)));
570 assert_eq!(c.get("b"), Some(&Value::Number(2)));
571 }
572
573 #[test]
574 fn expression_values_are_evaluated() {
575 let s = String::from("dynamic");
576 let c = ctx! { name: s };
577 assert_eq!(c.get("name"), Some(&Value::String("dynamic".into())));
578 }
579
580 #[test]
581 fn value_literal_passes_through() {
582 let c = ctx! { x: Value::Number(7) };
583 assert_eq!(c.get("x"), Some(&Value::Number(7)));
584 }
585}
586
587#[cfg(test)]
588mod entity_value_tests {
589 use super::*;
590 use crate::agreement::{AgreementFeatures, Gender, Number};
591
592 #[test]
593 fn entity_display_is_name() {
594 let v = Value::Entity {
595 name: "UserService".into(),
596 features: AgreementFeatures::default(),
597 };
598 assert_eq!(v.as_display(), "UserService");
599 }
600
601 #[test]
602 fn entity_as_list_is_none() {
603 let v = Value::Entity {
604 name: "X".into(),
605 features: AgreementFeatures::default(),
606 };
607 assert!(v.as_list().is_none());
608 }
609
610 #[test]
611 fn entity_as_number_is_none() {
612 let v = Value::Entity {
613 name: "Service".into(),
614 features: AgreementFeatures::default(),
615 };
616 assert!(v.as_number().is_none());
617 }
618
619 #[test]
620 fn entity_with_features_round_trips_via_equality() {
621 let features = AgreementFeatures::new()
622 .with_gender(Gender::Fem)
623 .with_number(Number::Singular);
624 let v1 = Value::Entity {
625 name: "Alice".into(),
626 features,
627 };
628 let v2 = v1.clone();
629 assert_eq!(v1, v2);
630 }
631
632 #[test]
633 fn entity_display_ignores_features() {
634 let v_plain = Value::Entity {
636 name: "Alice".into(),
637 features: AgreementFeatures::default(),
638 };
639 let v_with_features = Value::Entity {
640 name: "Alice".into(),
641 features: AgreementFeatures::new()
642 .with_gender(Gender::Fem)
643 .with_number(Number::Singular),
644 };
645 assert_eq!(v_plain.as_display(), v_with_features.as_display());
646 }
647}
648
649#[cfg(test)]
650mod entity_builder_tests {
651 use super::*;
652 use crate::agreement::{AgreementFeatures, Animacy, Case, Definiteness, Gender, Number};
653
654 #[test]
655 fn entity_helper_default_features() {
656 let ev = entity("UserService");
657 let v = ev.into_value();
658 match v {
659 Value::Entity { name, features } => {
660 assert_eq!(name, "UserService");
661 assert_eq!(features, AgreementFeatures::default());
662 }
663 _ => panic!("expected Value::Entity"),
664 }
665 }
666
667 #[test]
668 fn entity_builder_chain_sets_features() {
669 let v = entity("Alice")
670 .fem()
671 .sing()
672 .defined()
673 .animate()
674 .into_value();
675 match v {
676 Value::Entity { name, features } => {
677 assert_eq!(name, "Alice");
678 assert_eq!(features.gender, Gender::Fem);
679 assert_eq!(features.number, Number::Singular);
680 assert_eq!(features.definiteness, Definiteness::Definite);
681 assert_eq!(features.animacy, Animacy::Animate);
682 }
683 _ => panic!("expected Value::Entity"),
684 }
685 }
686
687 #[test]
688 fn entity_builder_all_gender_shortcuts() {
689 assert_eq!(
690 entity("x").masc().into_value(),
691 Value::Entity {
692 name: "x".into(),
693 features: AgreementFeatures::new().with_gender(Gender::Masc),
694 }
695 );
696 assert_eq!(
697 entity("x").fem().into_value(),
698 Value::Entity {
699 name: "x".into(),
700 features: AgreementFeatures::new().with_gender(Gender::Fem),
701 }
702 );
703 assert_eq!(
704 entity("x").neut().into_value(),
705 Value::Entity {
706 name: "x".into(),
707 features: AgreementFeatures::new().with_gender(Gender::Neut),
708 }
709 );
710 assert_eq!(
711 entity("x").common().into_value(),
712 Value::Entity {
713 name: "x".into(),
714 features: AgreementFeatures::new().with_gender(Gender::Common),
715 }
716 );
717 }
718
719 #[test]
720 fn entity_builder_all_number_shortcuts() {
721 assert_eq!(entity("x").plur().features.number, Number::Plural);
722 assert_eq!(entity("x").dual().features.number, Number::Dual);
723 assert_eq!(entity("x").sing().features.number, Number::Singular);
724 }
725
726 #[test]
727 fn entity_builder_definiteness_shortcuts() {
728 assert_eq!(
729 entity("x").defined().features.definiteness,
730 Definiteness::Definite
731 );
732 assert_eq!(
733 entity("x").indef().features.definiteness,
734 Definiteness::Indefinite
735 );
736 }
737
738 #[test]
739 fn entity_builder_animacy_shortcuts() {
740 assert_eq!(entity("x").animate().features.animacy, Animacy::Animate);
741 assert_eq!(entity("x").inanimate().features.animacy, Animacy::Inanimate);
742 }
743
744 #[test]
745 fn entity_builder_case_method() {
746 assert_eq!(
747 entity("x").case(Case::Genitive).features.case,
748 Case::Genitive
749 );
750 }
751
752 #[test]
753 fn entity_builder_with_features_override() {
754 let full = AgreementFeatures::new()
755 .with_gender(Gender::Fem)
756 .with_number(Number::Plural);
757 let v = entity("items").with_features(full).into_value();
758 match v {
759 Value::Entity { features, .. } => {
760 assert_eq!(features.gender, Gender::Fem);
761 assert_eq!(features.number, Number::Plural);
762 }
763 _ => panic!("expected Value::Entity"),
764 }
765 }
766
767 #[test]
768 fn ctx_macro_accepts_entity_value() {
769 let c = ctx! {
770 user: entity("Alice").fem().sing(),
771 count: 3,
772 };
773 match c.get("user").unwrap() {
774 Value::Entity { name, features } => {
775 assert_eq!(name, "Alice");
776 assert_eq!(features.gender, Gender::Fem);
777 }
778 _ => panic!("expected Value::Entity"),
779 }
780 assert_eq!(c.get("count"), Some(&Value::Number(3)));
781 }
782
783 #[test]
784 fn entity_build_and_into_value_are_equivalent() {
785 let ev1 = entity("TestService").fem();
786 let ev2 = ev1.clone();
787 assert_eq!(ev1.build(), ev2.into_value());
788 }
789}
790
791#[cfg(test)]
792mod has_schema_tests {
793 use super::*;
794 use prosaic_common::{ValueType, schema_lookup};
795
796 struct Manual;
797
798 impl HasProsaicSchema for Manual {
799 const PROSAIC_SCHEMA: &'static [(&'static str, ValueType)] =
800 &[("count", ValueType::Number), ("name", ValueType::String)];
801 }
802
803 #[test]
804 fn manual_impl_exposes_schema() {
805 assert_eq!(Manual::PROSAIC_SCHEMA.len(), 2);
806 }
807
808 #[test]
809 fn schema_is_const_queryable() {
810 const T: Option<ValueType> =
811 schema_lookup(<Manual as HasProsaicSchema>::PROSAIC_SCHEMA, "count");
812 assert_eq!(T, Some(ValueType::Number));
813 }
814}