sim_lib_standard_core/
observation.rs1use sim_kernel::{Datum, Error, Result, Symbol};
4
5pub type GuestValueProjection<T> = dyn Fn(&T) -> Result<Datum>;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum BoundedLane<T> {
12 Absent,
14 Complete(Vec<T>),
16 Truncated {
18 items: Vec<T>,
20 omitted: usize,
22 },
23}
24
25impl<T> BoundedLane<T> {
26 pub fn capture(items: Vec<T>, limit: usize) -> Self {
28 if items.len() <= limit {
29 Self::Complete(items)
30 } else {
31 let omitted = items.len() - limit;
32 Self::Truncated {
33 items: items.into_iter().take(limit).collect(),
34 omitted,
35 }
36 }
37 }
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct FailureLocation {
43 pub source: Symbol,
45 pub start: usize,
47 pub end: usize,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct CanonicalFailure {
54 pub class: Symbol,
56 pub detail: Datum,
58 pub location: Option<FailureLocation>,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub enum CanonicalOutcome {
65 Success(Datum),
67 Failure(CanonicalFailure),
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct CanonicalObservation {
74 pub outcome: Option<CanonicalOutcome>,
76 pub events: BoundedLane<Datum>,
78 pub receipts: BoundedLane<Datum>,
80 pub browse: BoundedLane<Datum>,
82}
83
84pub fn project_guest_value<T>(
89 value: &T,
90 projection: Option<&GuestValueProjection<T>>,
91) -> Result<Datum> {
92 projection.ok_or_else(|| {
93 Error::Eval("guest value has no canonical data face or profile projection".to_owned())
94 })?(value)
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 struct HostValue {
102 semantic: i64,
103 host_format: &'static str,
104 }
105
106 fn semantic_projection(value: &HostValue) -> Result<Datum> {
107 let _host_only_formatting = value.host_format;
108 Ok(Datum::String(value.semantic.to_string()))
109 }
110
111 fn observation(receipts: Vec<Datum>, location: FailureLocation) -> CanonicalObservation {
112 CanonicalObservation {
113 outcome: Some(CanonicalOutcome::Failure(CanonicalFailure {
114 class: Symbol::qualified("test", "rejected"),
115 detail: Datum::String("invalid-input".to_owned()),
116 location: Some(location),
117 })),
118 events: BoundedLane::Complete(vec![Datum::String("started".to_owned())]),
119 receipts: BoundedLane::capture(receipts, 4),
120 browse: BoundedLane::Complete(Vec::new()),
121 }
122 }
123
124 #[test]
125 fn guest_projection_ignores_host_formatting_and_is_mandatory() {
126 let terse = HostValue {
127 semantic: 7,
128 host_format: "7",
129 };
130 let verbose = HostValue {
131 semantic: 7,
132 host_format: "HostValue(7)",
133 };
134
135 assert_eq!(
136 project_guest_value(&terse, Some(&semantic_projection)).unwrap(),
137 project_guest_value(&verbose, Some(&semantic_projection)).unwrap()
138 );
139 assert!(project_guest_value(&terse, None).is_err());
140 }
141
142 #[test]
143 fn receipt_order_and_failure_location_are_semantic() {
144 let location = FailureLocation {
145 source: Symbol::qualified("fixture", "source"),
146 start: 2,
147 end: 5,
148 };
149 let first = Datum::String("first".to_owned());
150 let second = Datum::String("second".to_owned());
151 let baseline = observation(vec![first.clone(), second.clone()], location.clone());
152
153 assert_ne!(baseline, observation(vec![second, first], location.clone()));
154 assert_ne!(
155 baseline,
156 observation(
157 vec![
158 Datum::String("first".to_owned()),
159 Datum::String("second".to_owned())
160 ],
161 FailureLocation {
162 start: 3,
163 ..location
164 }
165 )
166 );
167 }
168
169 #[test]
170 fn absent_empty_and_truncated_lanes_remain_distinct() {
171 let empty = BoundedLane::<Datum>::capture(Vec::new(), 1);
172 let truncated = BoundedLane::capture(vec![Datum::Bool(true), Datum::Bool(false)], 1);
173
174 assert_ne!(BoundedLane::Absent, empty);
175 assert_ne!(empty, truncated);
176 assert_eq!(
177 truncated,
178 BoundedLane::Truncated {
179 items: vec![Datum::Bool(true)],
180 omitted: 1,
181 }
182 );
183 }
184}