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}