Skip to main content

harn_vm/schema/
mod.rs

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