Skip to main content

tools_kit/
schema.rs

1//! JSON Schema declarations, rendering preferences, and validation diagnostics.
2//!
3//! [`JsonSchema`](crate::schema::JsonSchema) is a passive wrapper. Owning execution boundaries validate
4//! arguments and successful structured output; strictness describes rendering
5//! preference and does not turn an invalid schema into a valid one.
6
7use serde::{Deserialize, Serialize};
8
9use crate::ToolError;
10
11/// Rendering strictness for a JSON Schema.
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum SchemaStrictness {
15    /// Require exact schema adherence where the source supports it.
16    Strict,
17    /// Request schema adherence without requiring source enforcement.
18    BestEffort,
19    /// Apply no portable strictness preference.
20    Unspecified,
21}
22
23/// JSON Schema plus portable strictness metadata.
24/// Construction wraps the document without compiling it. The owning Local/MCP
25/// operation validates schemas and values at its operation boundary.
26///
27/// # Examples
28///
29/// ```
30/// use tools_kit::schema::{JsonSchema, SchemaStrictness};
31/// let schema = JsonSchema::strict(serde_json::json!({"type":"object"}));
32/// assert_eq!(schema.schema["type"], "object");
33/// assert_eq!(schema.strictness, SchemaStrictness::Strict);
34/// ```
35///
36#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct JsonSchema {
39    /// JSON Schema document.
40    pub schema: serde_json::Value,
41    /// Portable strictness requested for the schema.
42    pub strictness: SchemaStrictness,
43}
44
45impl JsonSchema {
46    /// Wraps a schema with strict rendering semantics.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use tools_kit::schema::{JsonSchema, SchemaStrictness};
52    /// let schema = JsonSchema::strict(serde_json::json!({"type":"object"}));
53    /// assert_eq!(schema.schema["type"], "object");
54    /// assert_eq!(schema.strictness, SchemaStrictness::Strict);
55    /// ```
56    ///
57    #[must_use]
58    pub fn strict(schema: serde_json::Value) -> Self {
59        Self {
60            schema,
61            strictness: SchemaStrictness::Strict,
62        }
63    }
64
65    /// Wraps a schema with best-effort rendering semantics.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// use tools_kit::schema::{JsonSchema, SchemaStrictness};
71    /// let schema = JsonSchema::best_effort(serde_json::json!({"type":"object"}));
72    /// assert_eq!(schema.schema["type"], "object");
73    /// assert_eq!(schema.strictness, SchemaStrictness::BestEffort);
74    /// ```
75    ///
76    #[must_use]
77    pub fn best_effort(schema: serde_json::Value) -> Self {
78        Self {
79            schema,
80            strictness: SchemaStrictness::BestEffort,
81        }
82    }
83
84    /// Wraps a schema without a strictness preference.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use tools_kit::schema::{JsonSchema, SchemaStrictness};
90    /// let schema = JsonSchema::unspecified(serde_json::json!({"type":"object"}));
91    /// assert_eq!(schema.schema["type"], "object");
92    /// assert_eq!(schema.strictness, SchemaStrictness::Unspecified);
93    /// ```
94    ///
95    #[must_use]
96    pub fn unspecified(schema: serde_json::Value) -> Self {
97        Self {
98            schema,
99            strictness: SchemaStrictness::Unspecified,
100        }
101    }
102
103    pub(crate) fn validate(
104        &self,
105        value: &serde_json::Value,
106    ) -> Result<Vec<ValidationIssue>, ToolError> {
107        Self::validate_raw(&self.schema, value)
108    }
109
110    pub(crate) fn validate_raw(
111        schema: &serde_json::Value,
112        value: &serde_json::Value,
113    ) -> Result<Vec<ValidationIssue>, ToolError> {
114        let validator = jsonschema::validator_for(schema).map_err(|error| ToolError::Protocol {
115            message: format!("invalid JSON Schema: {error}"),
116        })?;
117        Ok(validator
118            .iter_errors(value)
119            .map(|error| ValidationIssue {
120                path: error.instance_path().to_string(),
121                message: error.to_string(),
122            })
123            .collect())
124    }
125}
126
127/// Structured validation diagnostic.
128///
129/// # Examples
130///
131/// ```
132/// let issue = tools_kit::schema::ValidationIssue { path: "/city".into(), message: "expected a string".into() };
133/// assert_eq!(issue.path, "/city");
134/// ```
135///
136#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(deny_unknown_fields)]
138pub struct ValidationIssue {
139    /// JSON Pointer-like path to the invalid value.
140    pub path: String,
141    /// Caller-safe description of the validation failure.
142    pub message: String,
143}