1mod collector;
29mod explain;
30mod jsonl;
31#[cfg(feature = "tracing")]
32mod tracing_tracer;
33
34pub use collector::AuditCollector;
35pub use explain::{Explanation, ExplanationLine, explain};
36pub use jsonl::JsonlTracer;
37#[cfg(feature = "tracing")]
38pub use tracing_tracer::TracingTracer;
39
40use serde::{Deserialize, Serialize};
41use serde_json::Value as JsonValue;
42
43use crate::ast::{Node, NodeId, Span};
44use crate::error::Error;
45use crate::value::{Inspect, MAX_NESTING, Value, nest};
46
47pub trait Tracer {
53 fn begin(&mut self, info: &EvalInfo) {
55 let _ = info;
56 }
57 fn enter(&mut self, node: &Node, input: ValueRef<'_>) {
59 let _ = (node, input);
60 }
61 fn exit(&mut self, node: &Node, outcome: Outcome<'_>) {
63 let _ = (node, outcome);
64 }
65 fn operands(&mut self, node: &Node, lhs: ValueRef<'_>, rhs: ValueRef<'_>) {
67 let _ = (node, lhs, rhs);
68 }
69 fn short_circuit(&mut self, node: &Node) {
71 let _ = node;
72 }
73 fn branch(&mut self, node: &Node, taken: Branch) {
75 let _ = (node, taken);
76 }
77 fn bind(&mut self, node: &Node, name: &str, value: ValueRef<'_>) {
79 let _ = (node, name, value);
80 }
81 fn filter_begin(&mut self, predicate: &Node) {
83 let _ = predicate;
84 }
85 fn filter_item(&mut self, predicate: &Node, index: usize, item: ValueRef<'_>, passed: bool) {
87 let _ = (predicate, index, item, passed);
88 }
89 fn filter_end(&mut self, predicate: &Node) {
91 let _ = predicate;
92 }
93 fn end(&mut self, outcome: Outcome<'_>) {
95 let _ = outcome;
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct EvalInfo<'a> {
102 pub source: &'a str,
104 pub timestamp: f64,
106}
107
108#[derive(Clone, Copy)]
110pub struct ValueRef<'a>(pub(crate) &'a dyn Inspect);
111
112impl ValueRef<'_> {
113 pub fn to_json(&self) -> Option<JsonValue> {
116 self.0.to_json()
117 }
118
119 pub fn capture(&self, limits: &CaptureLimits) -> Option<JsonValue> {
121 self.0.capture(limits)
122 }
123
124 pub fn is_undefined(&self) -> bool {
126 self.0.signature_symbol() == 'm'
127 }
128
129 pub fn as_bool(&self) -> Option<bool> {
131 self.0.as_bool()
132 }
133
134 pub fn type_name(&self) -> &'static str {
137 match self.0.signature_symbol() {
138 'm' => "undefined",
139 'l' => "null",
140 'b' => "boolean",
141 'n' => "number",
142 's' => "string",
143 'a' => "array",
144 'o' => "object",
145 _ => "function",
146 }
147 }
148}
149
150impl std::fmt::Debug for ValueRef<'_> {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 self.0.fmt(f)
153 }
154}
155
156#[derive(Debug, Clone, Copy)]
158pub enum Outcome<'a> {
159 Value(ValueRef<'a>),
161 Error(&'a Error),
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "lowercase")]
168pub enum Branch {
169 Then,
171 Else,
173 None,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
179#[serde(rename_all = "lowercase")]
180pub enum AuditLevel {
181 Off,
183 #[default]
186 Decisions,
187 Full,
189}
190
191#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct CaptureLimits {
195 pub max_items: usize,
197 pub max_string: usize,
199 pub max_depth: usize,
201 pub max_rejected: usize,
203 pub max_nodes: usize,
205 pub max_source: usize,
207}
208
209impl Default for CaptureLimits {
210 fn default() -> Self {
211 CaptureLimits {
212 max_items: 10,
213 max_string: 200,
214 max_depth: 4,
215 max_rejected: 5,
216 max_nodes: 10_000,
217 max_source: 200,
218 }
219 }
220}
221
222#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
224pub struct AuditOptions {
225 pub level: AuditLevel,
227 pub limits: CaptureLimits,
229}
230
231impl AuditOptions {
232 pub fn decisions() -> Self {
234 AuditOptions::default()
235 }
236
237 pub fn full() -> Self {
239 AuditOptions {
240 level: AuditLevel::Full,
241 ..AuditOptions::default()
242 }
243 }
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct AuditTree {
250 pub header: AuditHeader,
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub root: Option<AuditNode>,
255 pub node_count: usize,
257 pub truncated: bool,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264pub struct AuditHeader {
265 pub engine: String,
267 pub expression_hash: String,
269 pub input_hash: String,
271 pub timestamp: String,
273 pub duration_ms: f64,
275 pub level: AuditLevel,
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub result: Option<JsonValue>,
280 #[serde(skip_serializing_if = "Option::is_none")]
282 pub error: Option<AuditError>,
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
287pub struct AuditError {
288 pub code: String,
290 pub message: String,
292}
293
294impl From<&Error> for AuditError {
295 fn from(e: &Error) -> Self {
296 AuditError {
297 code: e.code().to_owned(),
298 message: e.message(),
299 }
300 }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305#[serde(rename_all = "lowercase")]
306pub enum OutcomeKind {
307 True,
309 False,
311 Value,
313 Undefined,
315 Error,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct AuditNode {
323 pub node: NodeId,
325 pub kind: String,
327 #[serde(skip_serializing_if = "Option::is_none")]
329 pub op: Option<String>,
330 pub span: Span,
332 pub source: String,
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub label: Option<String>,
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub message: Option<String>,
340 #[serde(skip_serializing_if = "Option::is_none")]
342 pub check_id: Option<String>,
343 #[serde(skip_serializing_if = "Option::is_none")]
345 pub input: Option<JsonValue>,
346 #[serde(skip_serializing_if = "Option::is_none")]
348 pub output: Option<JsonValue>,
349 pub outcome: OutcomeKind,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub error: Option<AuditError>,
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub operands: Option<[Option<JsonValue>; 2]>,
357 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
359 pub short_circuited: bool,
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub branch: Option<Branch>,
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub binding: Option<String>,
366 #[serde(skip_serializing_if = "Vec::is_empty", default)]
368 pub filters: Vec<FilterAudit>,
369 #[serde(skip_serializing_if = "Vec::is_empty", default)]
371 pub children: Vec<AuditNode>,
372}
373
374impl AuditNode {
375 pub fn passed(&self) -> Option<bool> {
378 match self.outcome {
379 OutcomeKind::True => Some(true),
380 OutcomeKind::False => Some(false),
381 _ => None,
382 }
383 }
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct FilterAudit {
390 pub node: NodeId,
392 pub source: String,
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub label: Option<String>,
397 pub tested: usize,
399 pub passed: usize,
401 #[serde(skip_serializing_if = "Vec::is_empty", default)]
403 pub rejected: Vec<JsonValue>,
404}
405
406pub(crate) fn capture(v: &Value<'_>, limits: &CaptureLimits, depth: usize) -> Option<JsonValue> {
409 Some(match v {
410 Value::Undefined => return None,
411 Value::Function(_) => JsonValue::String("<function>".into()),
412 Value::String(s) => {
413 let count = s.chars().count();
414 if count > limits.max_string {
415 let mut t: String = s.chars().take(limits.max_string).collect();
416 t.push('…');
417 JsonValue::String(t)
418 } else {
419 JsonValue::String(s.to_string())
420 }
421 }
422 Value::Array(_) | Value::Object(_) if depth >= limits.max_depth.min(MAX_NESTING) => {
423 JsonValue::String("…".into())
424 }
425 Value::Array(a) => {
426 let mut items: Vec<JsonValue> = a
427 .items()
428 .iter()
429 .take(limits.max_items)
430 .map(|i| {
431 nest(depth + 1, || capture(i, limits, depth + 1)).unwrap_or(JsonValue::Null)
432 })
433 .collect();
434 if a.items().len() > limits.max_items {
435 items.push(JsonValue::String(format!(
436 "… {} more",
437 a.items().len() - limits.max_items
438 )));
439 }
440 JsonValue::Array(items)
441 }
442 Value::Object(o) => {
443 let o = o.borrow();
444 let mut map = serde_json::Map::new();
445 for (k, v) in o.iter().take(limits.max_items) {
446 if let Some(j) = nest(depth + 1, || capture(v, limits, depth + 1)) {
447 map.insert(k.to_string(), j);
448 }
449 }
450 if o.len() > limits.max_items {
451 map.insert(
452 "…".into(),
453 JsonValue::String(format!("{} more", o.len() - limits.max_items)),
454 );
455 }
456 JsonValue::Object(map)
457 }
458 other => other.to_json()?,
459 })
460}
461
462pub(crate) fn fnv1a(text: &str) -> String {
464 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
465 for b in text.bytes() {
466 hash ^= u64::from(b);
467 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
468 }
469 format!("{hash:016x}")
470}
471
472pub(crate) fn describe(node: &Node) -> (&'static str, Option<String>) {
474 use crate::ast::NodeKind;
475 let op = match &node.kind {
476 NodeKind::Binary { op, .. } => Some(op.symbol().to_owned()),
477 NodeKind::Function { procedure, .. } | NodeKind::Partial { procedure, .. } => {
478 match &procedure.kind {
479 NodeKind::Variable { name } => Some(format!("${name}")),
480 NodeKind::Path { steps, .. } => match &steps[0].kind {
481 NodeKind::Variable { name } => Some(format!("${name}")),
482 NodeKind::Name { name } => Some(name.clone()),
483 _ => None,
484 },
485 _ => None,
486 }
487 }
488 NodeKind::Variable { name } => Some(format!("${name}")),
489 NodeKind::Name { name } => Some(name.clone()),
490 NodeKind::Bind { lhs, .. } => match &lhs.kind {
491 NodeKind::Variable { name } => Some(format!("${name}")),
492 _ => None,
493 },
494 _ => None,
495 };
496 (node.type_name(), op)
497}
498
499pub(crate) fn snippet(source: &str, span: Span, max: usize) -> String {
501 let text = source.get(span.start..span.end).unwrap_or_default();
502 let mut without_comments = String::with_capacity(text.len());
504 let mut rest = text;
505 while let Some(start) = rest.find("/*") {
506 without_comments.push_str(&rest[..start]);
507 match rest[start + 2..].find("*/") {
508 Some(end) => rest = &rest[start + 2 + end + 2..],
509 None => rest = "",
510 }
511 }
512 without_comments.push_str(rest);
513 let collapsed = without_comments
514 .split_whitespace()
515 .collect::<Vec<_>>()
516 .join(" ");
517 if collapsed.chars().count() > max {
518 let mut s: String = collapsed.chars().take(max).collect();
519 s.push('…');
520 s
521 } else {
522 collapsed
523 }
524}