Skip to main content

prosaic_core/
context.rs

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/// A value that can be inserted into a rendering context.
12///
13/// The [`Value::Entity`] variant carries a named entity with optional
14/// grammatical agreement features for multilingual rendering. In English
15/// it renders identically to [`Value::String`] (just the name); non-English
16/// grammars can inspect the `features` field for gender, number, case, etc.
17#[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    /// A named entity carrying agreement features for multilingual rendering.
24    ///
25    /// In English rendering this behaves identically to `Value::String(name)`.
26    /// Non-English grammars consult `features` to produce correctly-agreeing
27    /// articles, adjectives, pronouns, and verb forms.
28    Entity {
29        name: String,
30        #[cfg_attr(feature = "serde", serde(default))]
31        features: AgreementFeatures,
32    },
33}
34
35impl Value {
36    /// Render this value as a display string.
37    ///
38    /// For [`Value::Entity`] this returns the entity's name — identical
39    /// to the behaviour of [`Value::String`].
40    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    /// Try to interpret this value as a number.
53    pub fn as_number(&self) -> Option<i64> {
54        match self {
55            Value::Number(n) => Some(*n),
56            _ => None,
57        }
58    }
59
60    /// Try to interpret this value as a list of strings.
61    ///
62    /// Returns `None` for [`Value::Entity`] — entities are not lists.
63    pub fn as_list(&self) -> Option<&[String]> {
64        match self {
65            Value::List(items) => Some(items),
66            _ => None,
67        }
68    }
69}
70
71/// Holds the key-value pairs passed to a template for rendering.
72#[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    /// Insert a value into the context.
85    pub fn insert(&mut self, key: impl Into<String>, value: Value) {
86        self.values.insert(key.into(), value);
87    }
88
89    /// Look up a value by key.
90    pub fn get(&self, key: &str) -> Option<&Value> {
91        self.values.get(key)
92    }
93
94    /// Iterate over all keys in the context.
95    pub fn keys(&self) -> impl Iterator<Item = &String> {
96        self.values.keys()
97    }
98
99    /// Iterate over all key-value pairs in the context.
100    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
101        self.values.iter().map(|(k, v)| (k.as_str(), v))
102    }
103}
104
105/// Convert a host value into a [`Value`]. Used by the `ctx!` macro to
106/// accept strings, integers, and slices without requiring the caller to
107/// wrap each in a `Value::*` constructor.
108///
109/// This trait is intentionally narrow: impls are shipped for the common host
110/// types and third-party extension is not expected. For bespoke types, convert
111/// to a supported primitive first (e.g. via `Display`) or use the longer form
112/// `Value::String("...".into())`.
113///
114/// Numeric conversion: `i8`..`i64`, `u8`..`u32`, and `isize` go through an
115/// infallible `as i64` cast (their full range fits). `u64` and `usize` use a
116/// **saturating** `TryFrom` conversion — values above `i64::MAX` clamp to
117/// `i64::MAX` rather than wrapping to a negative. If you need the raw bit
118/// pattern, construct a `Value::Number(v as i64)` explicitly.
119pub 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
150// Saturating impls for unsigned wide integers (on 64-bit, usize == u64 and
151// values above i64::MAX would silently wrap to negative under a raw `as` cast).
152impl 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/// Fluent builder for entity-typed context values with agreement features.
201///
202/// Produced by [`entity()`]; consumed into a [`Value::Entity`] via
203/// [`IntoValue::into_value`] or [`EntityValue::build`].
204///
205/// # Example
206///
207/// ```
208/// use prosaic_core::{ctx, entity, Value};
209/// use prosaic_core::agreement::{Gender, Number, Definiteness};
210///
211/// let c = ctx! {
212///     user: entity("Alice").fem().sing().defined(),
213///     service: entity("UserService"),
214/// };
215///
216/// match c.get("user").unwrap() {
217///     Value::Entity { name, features } => {
218///         assert_eq!(name, "Alice");
219///         assert_eq!(features.gender, Gender::Fem);
220///     }
221///     _ => panic!("expected Entity"),
222/// }
223/// ```
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct EntityValue {
226    name: String,
227    features: crate::agreement::AgreementFeatures,
228}
229
230impl EntityValue {
231    // ── Gender shortcuts ──────────────────────────────────────────────────
232
233    /// Set gender to masculine.
234    pub fn masc(mut self) -> Self {
235        self.features.gender = crate::agreement::Gender::Masc;
236        self
237    }
238
239    /// Set gender to feminine.
240    pub fn fem(mut self) -> Self {
241        self.features.gender = crate::agreement::Gender::Fem;
242        self
243    }
244
245    /// Set gender to neuter.
246    pub fn neut(mut self) -> Self {
247        self.features.gender = crate::agreement::Gender::Neut;
248        self
249    }
250
251    /// Set gender to common (Dutch / Scandinavian 2-gender systems).
252    pub fn common(mut self) -> Self {
253        self.features.gender = crate::agreement::Gender::Common;
254        self
255    }
256
257    // ── Number shortcuts ──────────────────────────────────────────────────
258
259    /// Set number to singular.
260    pub fn sing(mut self) -> Self {
261        self.features.number = crate::agreement::Number::Singular;
262        self
263    }
264
265    /// Set number to plural.
266    pub fn plur(mut self) -> Self {
267        self.features.number = crate::agreement::Number::Plural;
268        self
269    }
270
271    /// Set number to dual (Arabic, Slovenian, Biblical Hebrew).
272    pub fn dual(mut self) -> Self {
273        self.features.number = crate::agreement::Number::Dual;
274        self
275    }
276
277    // ── Definiteness shortcuts ────────────────────────────────────────────
278
279    /// Set definiteness to definite.
280    pub fn defined(mut self) -> Self {
281        self.features.definiteness = crate::agreement::Definiteness::Definite;
282        self
283    }
284
285    /// Set definiteness to indefinite.
286    pub fn indef(mut self) -> Self {
287        self.features.definiteness = crate::agreement::Definiteness::Indefinite;
288        self
289    }
290
291    // ── Animacy shortcuts ─────────────────────────────────────────────────
292
293    /// Set animacy to animate.
294    pub fn animate(mut self) -> Self {
295        self.features.animacy = crate::agreement::Animacy::Animate;
296        self
297    }
298
299    /// Set animacy to inanimate.
300    pub fn inanimate(mut self) -> Self {
301        self.features.animacy = crate::agreement::Animacy::Inanimate;
302        self
303    }
304
305    // ── Case and person ───────────────────────────────────────────────────
306
307    /// Set the grammatical case.
308    pub fn case(mut self, c: crate::agreement::Case) -> Self {
309        self.features.case = c;
310        self
311    }
312
313    /// Set the grammatical person.
314    pub fn person(mut self, p: crate::agreement::AgreementPerson) -> Self {
315        self.features.person = p;
316        self
317    }
318
319    // ── Full-feature override ─────────────────────────────────────────────
320
321    /// Replace all features at once with a pre-built [`AgreementFeatures`].
322    pub fn with_features(mut self, f: crate::agreement::AgreementFeatures) -> Self {
323        self.features = f;
324        self
325    }
326
327    // ── Consume ───────────────────────────────────────────────────────────
328
329    /// Consume this builder into a [`Value::Entity`].
330    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
344/// Create an [`EntityValue`] for a named entity with default (unknown)
345/// agreement features. Chain builder methods to set gender, number, etc.
346///
347/// ```
348/// use prosaic_core::{ctx, entity, Value};
349/// use prosaic_core::agreement::Gender;
350///
351/// let c = ctx! {
352///     user: entity("Alice").fem().sing().defined(),
353///     service: entity("UserService"),  // features stay Unknown — English default
354/// };
355/// match c.get("user").unwrap() {
356///     Value::Entity { name, .. } => assert_eq!(name, "Alice"),
357///     _ => panic!(),
358/// }
359/// ```
360pub fn entity(name: impl Into<String>) -> EntityValue {
361    EntityValue {
362        name: name.into(),
363        features: crate::agreement::AgreementFeatures::default(),
364    }
365}
366
367/// Build a [`Context`] from key/value pairs. Values may be any type that
368/// implements [`IntoValue`] — `&str`, `String`, integer types, `bool`,
369/// `Vec<&str>`, `[&str; N]`, or an explicit `Value::*`.
370///
371/// Trailing commas are allowed. Empty `ctx! {}` produces an empty context.
372///
373/// # Example
374///
375/// ```
376/// use prosaic_core::{ctx, Context, Value};
377///
378/// let c: Context = ctx! {
379///     entity_type: "class",
380///     name: "UserService",
381///     consumer_count: 3,
382///     consumers: ["ProfileComponent", "SettingsComponent", "AdminModule"],
383/// };
384///
385/// assert_eq!(c.get("entity_type"), Some(&Value::String("class".into())));
386/// assert_eq!(c.get("consumer_count"), Some(&Value::Number(3)));
387/// ```
388#[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
400/// Convenience trait for converting types into `Context`.
401pub 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
411// Allow passing &Context to render methods that accept impl IntoContext
412// by cloning. This is ergonomic for callers that reuse a context.
413impl IntoContext for &Context {
414    fn into_context(self) -> Context {
415        self.clone()
416    }
417}
418
419/// Compile-time schema for a context type.
420///
421/// Deriving `#[derive(IntoContext)]` automatically implements this trait
422/// using the field names and Rust types of the struct. Hand-written impls
423/// are also supported for types that need `ValueType::Entity` slots or
424/// other mappings the derive does not produce.
425///
426/// The `PROSAIC_SCHEMA` constant is queryable at const evaluation time, so
427/// the `prosaic_template!` macro can emit per-slot assertions against it
428/// using [`prosaic_common::schema_lookup`] and
429/// [`prosaic_common::types_compatible`].
430pub 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        // u64::MAX is outside i64 range → saturate to i64::MAX, not silently wrap.
485        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        // On 64-bit platforms usize == u64 and usize::MAX saturates; on 32-bit
498        // platforms usize fits in i64 trivially, and the value is preserved.
499        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        // The name is all that as_display returns — features are invisible.
635        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}