1use std::collections::BTreeMap;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::canonical::{CanonicalError, canonical_value_bytes, tagged_digest};
14use crate::definition::ProvenanceClass;
15use crate::time::Timestamp;
16
17pub const VALUE_TAG: &str = "rustev.value/1";
19
20pub fn value_digest(value: &Value) -> Result<String, CanonicalError> {
23 Ok(tagged_digest(VALUE_TAG, &canonical_value_bytes(value)?))
24}
25
26#[derive(
29 Debug,
30 Clone,
31 Copy,
32 PartialEq,
33 Eq,
34 PartialOrd,
35 Ord,
36 Hash,
37 Serialize,
38 Deserialize,
39 schemars::JsonSchema,
40)]
41#[serde(rename_all = "snake_case")]
42pub enum Grade {
43 Hint,
46 Estimated,
48 Cached,
50 Authoritative,
52}
53
54impl Grade {
55 pub const ALL: [Grade; 4] = [
57 Grade::Authoritative,
58 Grade::Cached,
59 Grade::Estimated,
60 Grade::Hint,
61 ];
62
63 pub fn name(self) -> &'static str {
64 match self {
65 Grade::Authoritative => "authoritative",
66 Grade::Cached => "cached",
67 Grade::Estimated => "estimated",
68 Grade::Hint => "hint",
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
76#[serde(rename_all = "snake_case")]
77pub enum ValueGrade {
78 Authoritative,
79 Cached,
80 Estimated,
81 Hint,
82 Ungraded,
83}
84
85impl ValueGrade {
86 pub fn graded(self) -> Option<Grade> {
87 match self {
88 ValueGrade::Authoritative => Some(Grade::Authoritative),
89 ValueGrade::Cached => Some(Grade::Cached),
90 ValueGrade::Estimated => Some(Grade::Estimated),
91 ValueGrade::Hint => Some(Grade::Hint),
92 ValueGrade::Ungraded => None,
93 }
94 }
95
96 pub fn combine(self, other: ValueGrade) -> ValueGrade {
99 match (self.graded(), other.graded()) {
100 (Some(a), Some(b)) => a.min(b).into(),
101 (Some(a), None) | (None, Some(a)) => a.into(),
102 (None, None) => ValueGrade::Ungraded,
103 }
104 }
105
106 pub fn name(self) -> &'static str {
107 match self.graded() {
108 Some(g) => g.name(),
109 None => "ungraded",
110 }
111 }
112}
113
114impl From<Grade> for ValueGrade {
115 fn from(g: Grade) -> Self {
116 match g {
117 Grade::Authoritative => ValueGrade::Authoritative,
118 Grade::Cached => ValueGrade::Cached,
119 Grade::Estimated => ValueGrade::Estimated,
120 Grade::Hint => ValueGrade::Hint,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
127#[serde(rename_all = "snake_case", deny_unknown_fields)]
128pub enum AsOf {
129 None,
130 Ms(Timestamp),
131}
132
133impl AsOf {
134 pub fn oldest(self, other: AsOf) -> AsOf {
136 match (self, other) {
137 (AsOf::Ms(a), AsOf::Ms(b)) => AsOf::Ms(a.min(b)),
138 (AsOf::Ms(a), AsOf::None) | (AsOf::None, AsOf::Ms(a)) => AsOf::Ms(a),
139 (AsOf::None, AsOf::None) => AsOf::None,
140 }
141 }
142}
143
144#[derive(
146 Debug,
147 Clone,
148 Copy,
149 PartialEq,
150 Eq,
151 PartialOrd,
152 Ord,
153 Hash,
154 Serialize,
155 Deserialize,
156 schemars::JsonSchema,
157)]
158#[serde(rename_all = "snake_case")]
159pub enum MergeSide {
160 Base,
161 Overlay,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
166#[serde(rename_all = "snake_case", deny_unknown_fields)]
167pub enum Basis {
168 Acquired {
170 step: String,
171 },
172 Declared,
174 Computed,
176 Corroborated {
177 step: String,
178 },
179 Merged {
180 step: String,
181 side: MergeSide,
182 },
183 Resolved {
185 input: String,
186 },
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
193#[serde(deny_unknown_fields)]
194pub struct ValueMeta {
195 pub grade: ValueGrade,
196 pub provenance: Vec<ProvenanceClass>,
198 pub sources: Vec<String>,
200 pub as_of_ms: AsOf,
201 pub basis: Basis,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub fields: Option<BTreeMap<String, ValueMeta>>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub items: Option<Vec<ValueMeta>>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub entries: Option<BTreeMap<String, ValueMeta>>,
208}
209
210impl ValueMeta {
211 pub fn ungraded(basis: Basis) -> Self {
213 ValueMeta {
214 grade: ValueGrade::Ungraded,
215 provenance: vec![],
216 sources: vec![],
217 as_of_ms: AsOf::None,
218 basis,
219 fields: None,
220 items: None,
221 entries: None,
222 }
223 }
224
225 pub fn combination<'a>(parts: impl IntoIterator<Item = &'a ValueMeta>, basis: Basis) -> Self {
228 let mut out = ValueMeta::ungraded(basis);
229 for p in parts {
230 out.grade = out.grade.combine(p.grade);
231 out.provenance.extend(p.provenance.iter().copied());
232 out.sources.extend(p.sources.iter().cloned());
233 out.as_of_ms = out.as_of_ms.oldest(p.as_of_ms);
234 }
235 out.provenance.sort();
236 out.provenance.dedup();
237 out.sources.sort();
238 out.sources.dedup();
239 out
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
246#[serde(rename_all = "snake_case", deny_unknown_fields)]
247pub enum PresentWhen {
248 AnyNonNull(Vec<String>),
249 AllNonNull(Vec<String>),
250 All(Vec<PresentWhen>),
251 Any(Vec<PresentWhen>),
252}
253
254impl PresentWhen {
255 pub fn canonical(&self) -> PresentWhen {
257 let sorted = |v: &[String]| {
258 let mut v = v.to_vec();
259 v.sort();
260 v.dedup();
261 v
262 };
263 match self {
264 PresentWhen::AnyNonNull(f) => PresentWhen::AnyNonNull(sorted(f)),
265 PresentWhen::AllNonNull(f) => PresentWhen::AllNonNull(sorted(f)),
266 PresentWhen::All(ps) => PresentWhen::All(ps.iter().map(Self::canonical).collect()),
267 PresentWhen::Any(ps) => PresentWhen::Any(ps.iter().map(Self::canonical).collect()),
268 }
269 }
270
271 pub fn shape(&self) -> (usize, usize) {
273 match self {
274 PresentWhen::AnyNonNull(_) | PresentWhen::AllNonNull(_) => (1, 1),
275 PresentWhen::All(ps) | PresentWhen::Any(ps) => ps.iter().fold((1, 0), |(d, n), p| {
276 let (pd, pn) = p.shape();
277 (d.max(pd + 1), n.saturating_add(pn))
278 }),
279 }
280 }
281
282 pub fn fields(&self, out: &mut Vec<String>) {
284 match self {
285 PresentWhen::AnyNonNull(f) | PresentWhen::AllNonNull(f) => {
286 out.extend(f.iter().cloned())
287 }
288 PresentWhen::All(ps) | PresentWhen::Any(ps) => ps.iter().for_each(|p| p.fields(out)),
289 }
290 }
291
292 pub fn holds(&self, is_null: &dyn Fn(&str) -> bool) -> bool {
294 match self {
295 PresentWhen::AnyNonNull(f) => f.iter().any(|x| !is_null(x)),
296 PresentWhen::AllNonNull(f) => f.iter().all(|x| !is_null(x)),
297 PresentWhen::All(ps) => ps.iter().all(|p| p.holds(is_null)),
298 PresentWhen::Any(ps) => ps.iter().any(|p| p.holds(is_null)),
299 }
300 }
301}
302
303#[derive(
305 Debug,
306 Clone,
307 Copy,
308 PartialEq,
309 Eq,
310 PartialOrd,
311 Ord,
312 Hash,
313 Serialize,
314 Deserialize,
315 schemars::JsonSchema,
316)]
317#[serde(rename_all = "snake_case")]
318pub enum AbsentReason {
319 PredicateFailed,
320 OverlayMissing,
321 BaseMissing,
322 OverlayMissingBaseBelowGrade,
323 OverlayAndBaseMissing,
324 AcceptanceFailed {
325 cause: crate::extraction_results::AcceptanceFailure,
326 },
327}
328
329impl AbsentReason {
330 pub fn name(self) -> &'static str {
331 match self {
332 AbsentReason::AcceptanceFailed { .. } => "acceptance_failed",
333 AbsentReason::PredicateFailed => "predicate_failed",
334 AbsentReason::OverlayMissing => "overlay_missing",
335 AbsentReason::BaseMissing => "base_missing",
336 AbsentReason::OverlayMissingBaseBelowGrade => "overlay_missing_base_below_grade",
337 AbsentReason::OverlayAndBaseMissing => "overlay_and_base_missing",
338 }
339 }
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
344#[serde(rename_all = "snake_case", deny_unknown_fields)]
345pub enum Presence {
346 Present,
347 Absent {
348 reason: AbsentReason,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
351 predicate: Option<PresentWhen>,
352 },
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn grade_order_and_combination() {
361 assert!(Grade::Authoritative > Grade::Cached);
362 assert!(Grade::Cached > Grade::Estimated);
363 assert!(Grade::Estimated > Grade::Hint);
364 let a = ValueGrade::Authoritative;
365 assert_eq!(a.combine(ValueGrade::Hint), ValueGrade::Hint);
366 assert_eq!(a.combine(ValueGrade::Ungraded), a, "ungraded never lowers");
367 assert_eq!(
368 ValueGrade::Ungraded.combine(ValueGrade::Ungraded),
369 ValueGrade::Ungraded
370 );
371 assert_eq!(
372 ValueGrade::Hint.combine(ValueGrade::Ungraded),
373 ValueGrade::Hint,
374 "ungraded never raises"
375 );
376 }
377
378 #[test]
379 fn present_when_shape_and_canonical_form() {
380 let p = PresentWhen::All(vec![
381 PresentWhen::AnyNonNull(vec!["b".into(), "a".into(), "a".into()]),
382 PresentWhen::AllNonNull(vec!["c".into()]),
383 ]);
384 assert_eq!(p.shape(), (2, 2));
385 assert_eq!(
386 p.canonical(),
387 PresentWhen::All(vec![
388 PresentWhen::AnyNonNull(vec!["a".into(), "b".into()]),
389 PresentWhen::AllNonNull(vec!["c".into()]),
390 ])
391 );
392 let null = |f: &str| f != "b";
394 assert!(!p.holds(&null));
395 }
396
397 #[test]
398 fn value_digest_is_tagged() {
399 let d = value_digest(&serde_json::json!({"b": 1, "a": "x"})).unwrap();
400 assert!(d.starts_with("sha256:") && d.len() == 71);
401 assert_eq!(
402 d,
403 value_digest(&serde_json::json!({"a": "x", "b": 1})).unwrap()
404 );
405 }
406}