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}