mod collector;
mod explain;
mod jsonl;
#[cfg(feature = "tracing")]
mod tracing_tracer;
pub use collector::AuditCollector;
pub use explain::{Explanation, ExplanationLine, explain};
pub use jsonl::JsonlTracer;
#[cfg(feature = "tracing")]
pub use tracing_tracer::TracingTracer;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::ast::{Node, NodeId, Span};
use crate::error::Error;
use crate::value::{Inspect, MAX_NESTING, Value, nest};
pub trait Tracer {
fn begin(&mut self, info: &EvalInfo) {
let _ = info;
}
fn enter(&mut self, node: &Node, input: ValueRef<'_>) {
let _ = (node, input);
}
fn exit(&mut self, node: &Node, outcome: Outcome<'_>) {
let _ = (node, outcome);
}
fn operands(&mut self, node: &Node, lhs: ValueRef<'_>, rhs: ValueRef<'_>) {
let _ = (node, lhs, rhs);
}
fn short_circuit(&mut self, node: &Node) {
let _ = node;
}
fn branch(&mut self, node: &Node, taken: Branch) {
let _ = (node, taken);
}
fn bind(&mut self, node: &Node, name: &str, value: ValueRef<'_>) {
let _ = (node, name, value);
}
fn filter_begin(&mut self, predicate: &Node) {
let _ = predicate;
}
fn filter_item(&mut self, predicate: &Node, index: usize, item: ValueRef<'_>, passed: bool) {
let _ = (predicate, index, item, passed);
}
fn filter_end(&mut self, predicate: &Node) {
let _ = predicate;
}
fn end(&mut self, outcome: Outcome<'_>) {
let _ = outcome;
}
}
#[derive(Debug, Clone)]
pub struct EvalInfo<'a> {
pub source: &'a str,
pub timestamp: f64,
}
#[derive(Clone, Copy)]
pub struct ValueRef<'a>(pub(crate) &'a dyn Inspect);
impl ValueRef<'_> {
pub fn to_json(&self) -> Option<JsonValue> {
self.0.to_json()
}
pub fn capture(&self, limits: &CaptureLimits) -> Option<JsonValue> {
self.0.capture(limits)
}
pub fn is_undefined(&self) -> bool {
self.0.signature_symbol() == 'm'
}
pub fn as_bool(&self) -> Option<bool> {
self.0.as_bool()
}
pub fn type_name(&self) -> &'static str {
match self.0.signature_symbol() {
'm' => "undefined",
'l' => "null",
'b' => "boolean",
'n' => "number",
's' => "string",
'a' => "array",
'o' => "object",
_ => "function",
}
}
}
impl std::fmt::Debug for ValueRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, Copy)]
pub enum Outcome<'a> {
Value(ValueRef<'a>),
Error(&'a Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Branch {
Then,
Else,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuditLevel {
Off,
#[default]
Decisions,
Full,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptureLimits {
pub max_items: usize,
pub max_string: usize,
pub max_depth: usize,
pub max_rejected: usize,
pub max_nodes: usize,
pub max_source: usize,
}
impl Default for CaptureLimits {
fn default() -> Self {
CaptureLimits {
max_items: 10,
max_string: 200,
max_depth: 4,
max_rejected: 5,
max_nodes: 10_000,
max_source: 200,
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct AuditOptions {
pub level: AuditLevel,
pub limits: CaptureLimits,
}
impl AuditOptions {
pub fn decisions() -> Self {
AuditOptions::default()
}
pub fn full() -> Self {
AuditOptions {
level: AuditLevel::Full,
..AuditOptions::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuditTree {
pub header: AuditHeader,
#[serde(skip_serializing_if = "Option::is_none")]
pub root: Option<AuditNode>,
pub node_count: usize,
pub truncated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuditHeader {
pub engine: String,
pub expression_hash: String,
pub input_hash: String,
pub timestamp: String,
pub duration_ms: f64,
pub level: AuditLevel,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<AuditError>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuditError {
pub code: String,
pub message: String,
}
impl From<&Error> for AuditError {
fn from(e: &Error) -> Self {
AuditError {
code: e.code().to_owned(),
message: e.message(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutcomeKind {
True,
False,
Value,
Undefined,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuditNode {
pub node: NodeId,
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub op: Option<String>,
pub span: Span,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub check_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<JsonValue>,
pub outcome: OutcomeKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<AuditError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operands: Option<[Option<JsonValue>; 2]>,
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
pub short_circuited: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<Branch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub binding: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub filters: Vec<FilterAudit>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub children: Vec<AuditNode>,
}
impl AuditNode {
pub fn passed(&self) -> Option<bool> {
match self.outcome {
OutcomeKind::True => Some(true),
OutcomeKind::False => Some(false),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FilterAudit {
pub node: NodeId,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
pub tested: usize,
pub passed: usize,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub rejected: Vec<JsonValue>,
}
pub(crate) fn capture(v: &Value<'_>, limits: &CaptureLimits, depth: usize) -> Option<JsonValue> {
Some(match v {
Value::Undefined => return None,
Value::Function(_) => JsonValue::String("<function>".into()),
Value::String(s) => {
let count = s.chars().count();
if count > limits.max_string {
let mut t: String = s.chars().take(limits.max_string).collect();
t.push('…');
JsonValue::String(t)
} else {
JsonValue::String(s.to_string())
}
}
Value::Array(_) | Value::Object(_) if depth >= limits.max_depth.min(MAX_NESTING) => {
JsonValue::String("…".into())
}
Value::Array(a) => {
let mut items: Vec<JsonValue> = a
.items()
.iter()
.take(limits.max_items)
.map(|i| {
nest(depth + 1, || capture(i, limits, depth + 1)).unwrap_or(JsonValue::Null)
})
.collect();
if a.items().len() > limits.max_items {
items.push(JsonValue::String(format!(
"… {} more",
a.items().len() - limits.max_items
)));
}
JsonValue::Array(items)
}
Value::Object(o) => {
let o = o.borrow();
let mut map = serde_json::Map::new();
for (k, v) in o.iter().take(limits.max_items) {
if let Some(j) = nest(depth + 1, || capture(v, limits, depth + 1)) {
map.insert(k.to_string(), j);
}
}
if o.len() > limits.max_items {
map.insert(
"…".into(),
JsonValue::String(format!("{} more", o.len() - limits.max_items)),
);
}
JsonValue::Object(map)
}
other => other.to_json()?,
})
}
pub(crate) fn fnv1a(text: &str) -> String {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for b in text.bytes() {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{hash:016x}")
}
pub(crate) fn describe(node: &Node) -> (&'static str, Option<String>) {
use crate::ast::NodeKind;
let op = match &node.kind {
NodeKind::Binary { op, .. } => Some(op.symbol().to_owned()),
NodeKind::Function { procedure, .. } | NodeKind::Partial { procedure, .. } => {
match &procedure.kind {
NodeKind::Variable { name } => Some(format!("${name}")),
NodeKind::Path { steps, .. } => match &steps[0].kind {
NodeKind::Variable { name } => Some(format!("${name}")),
NodeKind::Name { name } => Some(name.clone()),
_ => None,
},
_ => None,
}
}
NodeKind::Variable { name } => Some(format!("${name}")),
NodeKind::Name { name } => Some(name.clone()),
NodeKind::Bind { lhs, .. } => match &lhs.kind {
NodeKind::Variable { name } => Some(format!("${name}")),
_ => None,
},
_ => None,
};
(node.type_name(), op)
}
pub(crate) fn snippet(source: &str, span: Span, max: usize) -> String {
let text = source.get(span.start..span.end).unwrap_or_default();
let mut without_comments = String::with_capacity(text.len());
let mut rest = text;
while let Some(start) = rest.find("/*") {
without_comments.push_str(&rest[..start]);
match rest[start + 2..].find("*/") {
Some(end) => rest = &rest[start + 2 + end + 2..],
None => rest = "",
}
}
without_comments.push_str(rest);
let collapsed = without_comments
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
if collapsed.chars().count() > max {
let mut s: String = collapsed.chars().take(max).collect();
s.push('…');
s
} else {
collapsed
}
}