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    /// See [`crate::schema::JsonSchema`] for a complete example.
49    ///
50    #[must_use]
51    pub fn strict(schema: serde_json::Value) -> Self {
52        Self {
53            schema,
54            strictness: SchemaStrictness::Strict,
55        }
56    }
57
58    /// Wraps a schema with best-effort rendering semantics.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use tools_kit::schema::{JsonSchema, SchemaStrictness};
64    /// let schema = JsonSchema::best_effort(serde_json::json!({"type":"object"}));
65    /// assert_eq!(schema.schema["type"], "object");
66    /// assert_eq!(schema.strictness, SchemaStrictness::BestEffort);
67    /// ```
68    ///
69    #[must_use]
70    pub fn best_effort(schema: serde_json::Value) -> Self {
71        Self {
72            schema,
73            strictness: SchemaStrictness::BestEffort,
74        }
75    }
76
77    /// Wraps a schema without a strictness preference.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use tools_kit::schema::{JsonSchema, SchemaStrictness};
83    /// let schema = JsonSchema::unspecified(serde_json::json!({"type":"object"}));
84    /// assert_eq!(schema.schema["type"], "object");
85    /// assert_eq!(schema.strictness, SchemaStrictness::Unspecified);
86    /// ```
87    ///
88    #[must_use]
89    pub fn unspecified(schema: serde_json::Value) -> Self {
90        Self {
91            schema,
92            strictness: SchemaStrictness::Unspecified,
93        }
94    }
95
96    pub(crate) fn validate(
97        &self,
98        value: &serde_json::Value,
99    ) -> Result<Vec<ValidationIssue>, ToolError> {
100        Self::validate_raw(&self.schema, value)
101    }
102
103    pub(crate) fn validate_raw(
104        schema: &serde_json::Value,
105        value: &serde_json::Value,
106    ) -> Result<Vec<ValidationIssue>, ToolError> {
107        let validator = jsonschema::validator_for(schema).map_err(|error| ToolError::Protocol {
108            message: format!("invalid JSON Schema: {error}"),
109        })?;
110        Ok(validator
111            .iter_errors(value)
112            .map(|error| ValidationIssue {
113                path: error.instance_path().to_string(),
114                message: error.to_string(),
115            })
116            .collect())
117    }
118}
119
120/// Structured validation diagnostic.
121///
122/// # Examples
123///
124/// ```
125/// let issue = tools_kit::schema::ValidationIssue {
126///     path: "/city".into(),
127///     message: "expected a string".into(),
128/// };
129/// assert_eq!(issue.path, "/city");
130/// ```
131///
132#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134pub struct ValidationIssue {
135    /// JSON Pointer-like path to the invalid value.
136    pub path: String,
137    /// Caller-safe description of the validation failure.
138    pub message: String,
139}