Skip to main content

harn_vm/schema/
mod.rs

1use crate::value::VmValue;
2
3mod api;
4mod canonicalize;
5mod limits;
6mod result;
7mod transform;
8mod type_check;
9mod validate;
10
11pub(crate) use api::{
12    canonical_param_schema, schema_assert_canonical_param, schema_assert_param,
13    schema_expect_value, schema_extend_value, schema_from_json_schema_value,
14    schema_from_openapi_schema_value, schema_is_value, schema_omit_value, schema_partial_value,
15    schema_pick_value, schema_report_value, schema_result_value, schema_to_json_schema_value,
16    schema_to_openapi_schema_value, CanonicalParamSchema,
17};
18pub use canonicalize::json_to_vm_value;
19
20/// Canonicalize a JSON-Schema-shaped value for use as an MCP elicitation
21/// `requestedSchema`. Reuses the same canonicalizer the rest of the
22/// language uses for `schema_from_json_schema(...)` so behavior is
23/// identical between user-facing schema builtins and elicitation.
24pub fn elicitation_validate_schema(schema: &VmValue) -> Result<VmValue, crate::value::VmError> {
25    schema_from_json_schema_value(schema)
26}
27
28/// Validate `data` against a canonicalized schema. Mirrors the
29/// `schema_expect` semantics — returns the (possibly defaulted) value
30/// on success and a thrown error string on failure.
31pub fn elicitation_validate(
32    data: &VmValue,
33    schema: &VmValue,
34) -> Result<VmValue, crate::value::VmError> {
35    schema_expect_value(data, schema, false)
36}
37
38pub(crate) const BYTES_B64_TAG: &str = "$bytes_b64";
39
40pub(crate) fn tagged_bytes_json(bytes: &[u8]) -> serde_json::Value {
41    use base64::Engine;
42
43    serde_json::json!({
44        BYTES_B64_TAG: base64::engine::general_purpose::STANDARD.encode(bytes),
45    })
46}
47
48fn vm_value_to_serde_json(value: &VmValue) -> serde_json::Value {
49    match value {
50        VmValue::Nil => serde_json::Value::Null,
51        VmValue::Bool(value) => serde_json::Value::Bool(*value),
52        VmValue::Int(value) => serde_json::json!(value),
53        VmValue::Float(value) => serde_json::json!(value),
54        VmValue::String(value) => serde_json::Value::String(value.to_string()),
55        VmValue::Bytes(bytes) => tagged_bytes_json(bytes),
56        VmValue::List(items) => {
57            serde_json::Value::Array(items.iter().map(vm_value_to_serde_json).collect())
58        }
59        VmValue::Set(set) => {
60            serde_json::Value::Array(set.iter().map(vm_value_to_serde_json).collect())
61        }
62        VmValue::Dict(items) => serde_json::Value::Object(
63            items
64                .iter()
65                .map(|(key, value)| (key.to_string(), vm_value_to_serde_json(value)))
66                .collect(),
67        ),
68        _ => serde_json::Value::String(value.display()),
69    }
70}
71
72fn schema_bool(schema: &crate::value::DictMap, key: &str) -> bool {
73    matches!(schema.get(key), Some(VmValue::Bool(true)))
74}
75
76fn schema_i64(schema: &crate::value::DictMap, key: &str) -> Option<i64> {
77    match schema.get(key) {
78        Some(VmValue::Int(value)) => Some(*value),
79        _ => None,
80    }
81}
82
83fn schema_number(schema: &crate::value::DictMap, key: &str) -> Option<f64> {
84    match schema.get(key) {
85        Some(VmValue::Int(value)) => Some(*value as f64),
86        Some(VmValue::Float(value)) => Some(*value),
87        _ => None,
88    }
89}
90
91fn child_path(path: &str, key: &str) -> String {
92    if path.is_empty() {
93        key.to_string()
94    } else {
95        format!("{path}.{key}")
96    }
97}
98
99fn index_path(path: &str, index: usize) -> String {
100    if path.is_empty() {
101        format!("[{index}]")
102    } else {
103        format!("{path}[{index}]")
104    }
105}
106
107#[cfg(test)]
108mod tests;