Skip to main content

konjure_lang/
inspection.rs

1//! Read-only access to accepted values. Inspection never evaluates source or invokes a method.
2use crate::{Snapshot, Value};
3use konjure_sdk::data::{TensorInspection, TensorInspectionOptions, inspect_tensor};
4use serde::{Deserialize, Serialize};
5
6/// An accepted global binding or entity component from which to inspect data.
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
8#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
9#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
10pub enum InspectionRoot {
11    /// A canonical snapshot global name, including its module prefix when imported.
12    Global {
13        /// Exact key in [`Snapshot::globals`].
14        name: String,
15    },
16    /// One nominal component of a live entity.
17    Component {
18        /// Exact entity identity; the language bounds IDs to portable JSON integers.
19        #[cfg_attr(feature = "typescript", ts(type = "number"))]
20        entity: u64,
21        /// Exact nominal component key in the snapshot.
22        component: String,
23    },
24}
25
26/// A structural step through an accepted record or list, never executable code.
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
29#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
30pub enum InspectionStep {
31    /// Read a declared record field.
32    Field {
33        /// Exact field name.
34        name: String,
35    },
36    /// Read an existing list element by its nonnegative position.
37    Index {
38        /// Zero-based list position.
39        index: u32,
40    },
41    /// Read the payload of a selected algebraic enum case.
42    Payload {
43        /// Zero-based payload position in the declared case.
44        index: u32,
45    },
46}
47
48/// Identifies a value without retaining or serializing its entire payload.
49#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
50#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
51#[serde(deny_unknown_fields)]
52pub struct InspectionTarget {
53    /// Accepted binding or component containing the value.
54    pub root: InspectionRoot,
55    /// At most sixteen field/list steps from the root.
56    #[serde(default)]
57    pub path: Vec<InspectionStep>,
58}
59
60/// A rejected inspection request; these errors never change runtime state.
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
62#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
63#[serde(rename_all = "snake_case")]
64pub enum InspectionError {
65    /// The encoded request or its structural path exceeds inspection bounds.
66    InvalidTarget,
67    /// A binding, component, field, or list element is absent in this snapshot.
68    MissingValue,
69    /// The selected value is not a tensor.
70    NotTensor,
71    /// A tensor could not be inspected within the SDK's bounded contract.
72    InvalidTensor,
73    /// The host could not encode an inspection result.
74    Encoding,
75}
76
77impl Snapshot {
78    /// Resolves an inspection target through accepted data, without executing user code.
79    pub fn inspection_value(&self, target: &InspectionTarget) -> Result<&Value, InspectionError> {
80        let valid_name = |name: &str| !name.is_empty() && name.len() <= 512;
81        if target.path.len() > 16 {
82            return Err(InspectionError::InvalidTarget);
83        }
84        let mut value = match &target.root {
85            InspectionRoot::Global { name } if valid_name(name) => self.globals.get(name),
86            InspectionRoot::Component { entity, component } if valid_name(component) => self
87                .entities
88                .iter()
89                .find(|candidate| candidate.id == *entity)
90                .and_then(|candidate| candidate.components.get(component)),
91            _ => return Err(InspectionError::InvalidTarget),
92        }
93        .ok_or(InspectionError::MissingValue)?;
94        for step in &target.path {
95            value = match (step, value) {
96                (InspectionStep::Field { name }, Value::Record(record)) if valid_name(name) => {
97                    record.fields.get(name)
98                }
99                (InspectionStep::Index { index }, Value::List(values)) => {
100                    values.get(*index as usize)
101                }
102                (InspectionStep::Payload { index }, Value::Enum(value)) => {
103                    value.payload.get(*index as usize)
104                }
105                _ => None,
106            }
107            .ok_or(InspectionError::MissingValue)?;
108        }
109        Ok(value)
110    }
111
112    /// Computes a bounded tensor summary from this snapshot's immutable storage.
113    ///
114    /// The SDK reports whether statistics cover every element or a deterministic
115    /// sample. Wide numeric extrema retain their exact decimal representation.
116    pub fn tensor_inspection(
117        &self,
118        target: &InspectionTarget,
119    ) -> Result<TensorInspection, InspectionError> {
120        let Value::Tensor(tensor) = self.inspection_value(target)? else {
121            return Err(InspectionError::NotTensor);
122        };
123        inspect_tensor(tensor, TensorInspectionOptions::default())
124            .map_err(|_| InspectionError::InvalidTensor)
125    }
126}