Skip to main content

hedl_mcp/tools/
schema_macros.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Declarative macros for JSON schema generation.
19//!
20//! This module provides a comprehensive macro system to reduce boilerplate
21//! in schema definitions for MCP tools. The macros support common patterns:
22//! - String arguments (HEDL content, file paths, formats)
23//! - Boolean flags (ditto, strict, recursive, etc.)
24//! - Integer parameters (limit, offset, size constraints)
25//! - Optional JSON objects (format-specific options)
26//! - Enumerated types (format choices, tokenizers)
27//!
28//! # Examples
29//!
30//! ```text
31//! use crate::tools::schema_macros::*;
32//!
33//! // Simple string argument with description
34//! let schema = schema_object! {
35//!     hedl: schema_string!("HEDL document content to validate")
36//! };
37//!
38//! // String with enum constraints
39//! let schema = schema_object! {
40//!     format: schema_enum!(["json", "yaml", "csv"], "Target output format")
41//! };
42//!
43//! // Boolean with default
44//! let schema = schema_object! {
45//!     strict: schema_bool!("Enable strict validation", default: true)
46//! };
47//!
48//! // Complete tool schema with required fields
49//! let schema = tool_schema! {
50//!     required: ["hedl", "format"],
51//!     properties: {
52//!         hedl: schema_string!("HEDL content"),
53//!         format: schema_enum!(["json", "yaml"], "Output format"),
54//!         ditto: schema_bool!("Use ditto optimization", default: true)
55//!     }
56//! };
57//! ```
58
59/// Generate a JSON schema object with type "string" and description.
60///
61/// # Usage
62/// ```text
63/// schema_string!("Description of the string field")
64/// schema_string!("File path", pattern: r"\.hedl$")
65/// ```
66#[macro_export]
67macro_rules! schema_string {
68    ($description:expr) => {
69        serde_json::json!({
70            "type": "string",
71            "description": $description
72        })
73    };
74    ($description:expr, pattern: $pattern:expr) => {
75        serde_json::json!({
76            "type": "string",
77            "description": $description,
78            "pattern": $pattern
79        })
80    };
81}
82
83/// Generate a JSON schema object with type "boolean" and optional default.
84///
85/// # Usage
86/// ```text
87/// schema_bool!("Enable feature")
88/// schema_bool!("Enable strict mode", default: true)
89/// schema_bool!("Disable validation", default: false)
90/// ```
91#[macro_export]
92macro_rules! schema_bool {
93    ($description:expr) => {
94        serde_json::json!({
95            "type": "boolean",
96            "description": $description
97        })
98    };
99    ($description:expr, default: $default:expr) => {
100        serde_json::json!({
101            "type": "boolean",
102            "description": $description,
103            "default": $default
104        })
105    };
106}
107
108/// Generate a JSON schema object with type "integer" and optional constraints.
109///
110/// # Usage
111/// ```text
112/// schema_integer!("Number of items")
113/// schema_integer!("Maximum rows", minimum: 1, maximum: 1000)
114/// schema_integer!("Page size", default: 100)
115/// ```
116#[macro_export]
117macro_rules! schema_integer {
118    ($description:expr) => {
119        serde_json::json!({
120            "type": "integer",
121            "description": $description
122        })
123    };
124    ($description:expr, default: $default:expr) => {
125        serde_json::json!({
126            "type": "integer",
127            "description": $description,
128            "default": $default
129        })
130    };
131    ($description:expr, minimum: $min:expr, maximum: $max:expr) => {
132        serde_json::json!({
133            "type": "integer",
134            "description": $description,
135            "minimum": $min,
136            "maximum": $max
137        })
138    };
139}
140
141/// Generate a JSON schema object with string enum constraints.
142///
143/// # Usage
144/// ```text
145/// schema_enum!(["json", "yaml", "csv"], "Output format")
146/// schema_enum!(["simple", "cl100k"], "Tokenizer algorithm", default: "simple")
147/// ```
148#[macro_export]
149macro_rules! schema_enum {
150    ([$($variant:expr),+ $(,)?], $description:expr) => {
151        serde_json::json!({
152            "type": "string",
153            "enum": [$($variant),+],
154            "description": $description
155        })
156    };
157    ([$($variant:expr),+ $(,)?], $description:expr, default: $default:expr) => {
158        serde_json::json!({
159            "type": "string",
160            "enum": [$($variant),+],
161            "description": $description,
162            "default": $default
163        })
164    };
165}
166
167/// Generate a JSON schema object for optional format-specific options.
168///
169/// # Usage
170/// ```text
171/// schema_options! {
172///     pretty: schema_bool!("Pretty-print output (json)"),
173///     delimiter: schema_string!("Field delimiter (csv)")
174/// }
175/// ```
176#[macro_export]
177macro_rules! schema_options {
178    ($($field:ident: $schema:expr),+ $(,)?) => {
179        serde_json::json!({
180            "type": "object",
181            "description": "Format-specific options",
182            "properties": {
183                $(stringify!($field): $schema),+
184            }
185        })
186    };
187}
188
189/// Generate a JSON schema object with string array type.
190///
191/// # Usage
192/// ```text
193/// schema_string_array!("List of column names")
194/// schema_string_array!("Field names for schema inference", items_pattern: r"^[a-z_]+$")
195/// ```
196#[macro_export]
197macro_rules! schema_string_array {
198    ($description:expr) => {
199        serde_json::json!({
200            "type": "array",
201            "items": { "type": "string" },
202            "description": $description
203        })
204    };
205    ($description:expr, items_pattern: $pattern:expr) => {
206        serde_json::json!({
207            "type": "array",
208            "items": {
209                "type": "string",
210                "pattern": $pattern
211            },
212            "description": $description
213        })
214    };
215}
216
217/// Generate a JSON schema object with array type and custom items schema.
218///
219/// # Usage
220/// ```text
221/// schema_array!("List of operations", items: {"type": "object", "properties": {...}})
222/// ```
223#[macro_export]
224macro_rules! schema_array {
225    ($description:expr, items: $items:tt) => {
226        serde_json::json!({
227            "type": "array",
228            "items": $items,
229            "description": $description
230        })
231    };
232}
233
234/// Generate a complete tool schema with properties and required fields.
235///
236/// This is the top-level macro for defining entire tool schemas.
237///
238/// # Usage
239/// ```text
240/// tool_schema! {
241///     required: ["hedl"],
242///     properties: {
243///         hedl: schema_string!("HEDL document content"),
244///         format: schema_enum!(["json", "yaml"], "Output format"),
245///         ditto: schema_bool!("Use ditto optimization", default: true)
246///     }
247/// }
248/// ```
249#[macro_export]
250macro_rules! tool_schema {
251    (
252        required: [$($req:expr),* $(,)?],
253        properties: {
254            $($field:ident: $schema:expr),+ $(,)?
255        }
256    ) => {
257        serde_json::json!({
258            "type": "object",
259            "properties": {
260                $(stringify!($field): $schema),+
261            },
262            "required": [$($req),*]
263        })
264    };
265}
266
267/// Generate schema for HEDL content argument (with size validation).
268///
269/// This is a specialized macro for the common "hedl" string argument.
270///
271/// # Usage
272/// ```text
273/// hedl_content_arg!()
274/// hedl_content_arg!("Custom description")
275/// ```
276#[macro_export]
277macro_rules! hedl_content_arg {
278    () => {
279        $crate::schema_string!("HEDL document content")
280    };
281    ($description:expr) => {
282        $crate::schema_string!($description)
283    };
284}
285
286/// Generate schema for file path argument.
287///
288/// # Usage
289/// ```text
290/// path_arg!()
291/// path_arg!("Output file path")
292/// ```
293#[macro_export]
294macro_rules! path_arg {
295    () => {
296        $crate::schema_string!("File or directory path")
297    };
298    ($description:expr) => {
299        $crate::schema_string!($description)
300    };
301}
302
303/// Generate schema for format argument with enum values.
304///
305/// # Usage
306/// ```text
307/// format_arg!(["json", "yaml", "csv"])
308/// format_arg!(["json", "yaml", "csv"], "Source format to convert from")
309/// ```
310#[macro_export]
311macro_rules! format_arg {
312    ([$($variant:expr),+ $(,)?]) => {
313        $crate::schema_enum!([$($variant),+], "Format type")
314    };
315    ([$($variant:expr),+ $(,)?], $description:expr) => {
316        $crate::schema_enum!([$($variant),+], $description)
317    };
318}
319
320/// Generate schema for validation arguments (strict, lint).
321///
322/// # Usage
323/// ```text
324/// validation_args!()
325/// ```
326#[macro_export]
327macro_rules! validation_args {
328    () => {
329        (
330            $crate::schema_bool!(
331                "Enable strict validation mode: treat lint warnings as errors",
332                default: true
333            ),
334            $crate::schema_bool!(
335                "Run linting rules in addition to parsing",
336                default: true
337            )
338        )
339    };
340}
341
342/// Generate schema for pagination arguments (limit, offset).
343///
344/// # Usage
345/// ```text
346/// pagination_args!()
347/// pagination_args!(default_limit: 50)
348/// ```
349#[macro_export]
350macro_rules! pagination_args {
351    () => {
352        (
353            $crate::schema_integer!(
354                "Maximum number of entities to return",
355                default: 100
356            ),
357            $crate::schema_integer!(
358                "Number of entities to skip",
359                default: 0
360            )
361        )
362    };
363    (default_limit: $limit:expr) => {
364        (
365            $crate::schema_integer!(
366                "Maximum number of entities to return",
367                default: $limit
368            ),
369            $crate::schema_integer!(
370                "Number of entities to skip",
371                default: 0
372            )
373        )
374    };
375}
376
377/// Generate schema for file operation arguments (validate, format, backup).
378///
379/// # Usage
380/// ```text
381/// file_write_args!()
382/// ```
383#[macro_export]
384macro_rules! file_write_args {
385    () => {
386        (
387            $crate::schema_bool!(
388                "Validate HEDL before writing",
389                default: true
390            ),
391            $crate::schema_bool!(
392                "Format/canonicalize before writing",
393                default: false
394            ),
395            $crate::schema_bool!(
396                "Create backup of existing file if it exists",
397                default: true
398            )
399        )
400    };
401}
402
403/// Generate schema for ditto optimization argument.
404///
405/// # Usage
406/// ```text
407/// ditto_arg!()
408/// ditto_arg!("Apply ditto optimization for repeated values")
409/// ```
410#[macro_export]
411macro_rules! ditto_arg {
412    () => {
413        $crate::schema_bool!(
414            "Enable ditto optimization for repeated values",
415            default: true
416        )
417    };
418    ($description:expr) => {
419        $crate::schema_bool!($description, default: true)
420    };
421}
422
423/// Generate schema for conversion options (to format).
424///
425/// # Usage
426/// ```text
427/// convert_to_options!()
428/// ```
429#[macro_export]
430macro_rules! convert_to_options {
431    () => {
432        $crate::schema_options! {
433            pretty: $crate::schema_bool!("Pretty-print output (json)"),
434            include_headers: $crate::schema_bool!("Include headers (csv)"),
435            use_merge: $crate::schema_bool!("Use MERGE vs CREATE (cypher)"),
436            include_constraints: $crate::schema_bool!("Include constraints (cypher)")
437        }
438    };
439}
440
441/// Generate schema for conversion options (from format).
442///
443/// # Usage
444/// ```text
445/// convert_from_options!()
446/// ```
447#[macro_export]
448macro_rules! convert_from_options {
449    () => {
450        $crate::schema_options! {
451            type_name: $crate::schema_string!("Type name for entities (csv)"),
452            schema: $crate::schema_string_array!("Column names (csv)"),
453            delimiter: $crate::schema_string!("Field delimiter (csv)")
454        }
455    };
456}
457
458#[cfg(test)]
459mod tests {
460    use serde_json::Value as JsonValue;
461
462    #[test]
463    fn test_schema_string() {
464        let schema = schema_string!("Test description");
465        assert_eq!(schema["type"], "string");
466        assert_eq!(schema["description"], "Test description");
467        assert!(schema.get("pattern").is_none());
468    }
469
470    #[test]
471    fn test_schema_string_with_pattern() {
472        let schema = schema_string!("HEDL file", pattern: r"\.hedl$");
473        assert_eq!(schema["type"], "string");
474        assert_eq!(schema["description"], "HEDL file");
475        assert_eq!(schema["pattern"], r"\.hedl$");
476    }
477
478    #[test]
479    fn test_schema_bool() {
480        let schema = schema_bool!("Enable feature");
481        assert_eq!(schema["type"], "boolean");
482        assert_eq!(schema["description"], "Enable feature");
483        assert!(schema.get("default").is_none());
484    }
485
486    #[test]
487    fn test_schema_bool_with_default() {
488        let schema = schema_bool!("Enable strict mode", default: true);
489        assert_eq!(schema["type"], "boolean");
490        assert_eq!(schema["description"], "Enable strict mode");
491        assert_eq!(schema["default"], true);
492    }
493
494    #[test]
495    fn test_schema_integer() {
496        let schema = schema_integer!("Row count");
497        assert_eq!(schema["type"], "integer");
498        assert_eq!(schema["description"], "Row count");
499    }
500
501    #[test]
502    fn test_schema_integer_with_default() {
503        let schema = schema_integer!("Page size", default: 50);
504        assert_eq!(schema["type"], "integer");
505        assert_eq!(schema["default"], 50);
506    }
507
508    #[test]
509    fn test_schema_integer_with_range() {
510        let schema = schema_integer!("Port number", minimum: 1, maximum: 65535);
511        assert_eq!(schema["type"], "integer");
512        assert_eq!(schema["minimum"], 1);
513        assert_eq!(schema["maximum"], 65535);
514    }
515
516    #[test]
517    fn test_schema_enum() {
518        let schema = schema_enum!(["json", "yaml", "csv"], "Output format");
519        assert_eq!(schema["type"], "string");
520        assert_eq!(schema["description"], "Output format");
521        let enum_vals = schema["enum"].as_array().unwrap();
522        assert_eq!(enum_vals.len(), 3);
523        assert!(enum_vals.contains(&JsonValue::String("json".to_string())));
524    }
525
526    #[test]
527    fn test_schema_enum_with_default() {
528        let schema = schema_enum!(["simple", "cl100k"], "Tokenizer", default: "simple");
529        assert_eq!(schema["type"], "string");
530        assert_eq!(schema["default"], "simple");
531    }
532
533    #[test]
534    fn test_schema_options() {
535        let schema = schema_options! {
536            pretty: schema_bool!("Pretty output"),
537            indent: schema_integer!("Indent size")
538        };
539        assert_eq!(schema["type"], "object");
540        assert_eq!(schema["description"], "Format-specific options");
541        assert!(schema["properties"].get("pretty").is_some());
542        assert!(schema["properties"].get("indent").is_some());
543    }
544
545    #[test]
546    fn test_schema_string_array() {
547        let schema = schema_string_array!("Column names");
548        assert_eq!(schema["type"], "array");
549        assert_eq!(schema["items"]["type"], "string");
550        assert_eq!(schema["description"], "Column names");
551    }
552
553    #[test]
554    fn test_tool_schema() {
555        let schema = tool_schema! {
556            required: ["hedl", "format"],
557            properties: {
558                hedl: schema_string!("HEDL content"),
559                format: schema_enum!(["json", "yaml"], "Format"),
560                ditto: schema_bool!("Use ditto", default: true)
561            }
562        };
563        assert_eq!(schema["type"], "object");
564        let required = schema["required"].as_array().unwrap();
565        assert_eq!(required.len(), 2);
566        assert!(schema["properties"].get("hedl").is_some());
567        assert!(schema["properties"].get("format").is_some());
568        assert!(schema["properties"].get("ditto").is_some());
569    }
570
571    #[test]
572    fn test_hedl_content_arg() {
573        let schema = hedl_content_arg!();
574        assert_eq!(schema["type"], "string");
575        assert_eq!(schema["description"], "HEDL document content");
576
577        let custom = hedl_content_arg!("Custom HEDL description");
578        assert_eq!(custom["description"], "Custom HEDL description");
579    }
580
581    #[test]
582    fn test_path_arg() {
583        let schema = path_arg!();
584        assert_eq!(schema["type"], "string");
585        assert_eq!(schema["description"], "File or directory path");
586
587        let custom = path_arg!("Output directory");
588        assert_eq!(custom["description"], "Output directory");
589    }
590
591    #[test]
592    fn test_format_arg() {
593        let schema = format_arg!(["json", "yaml", "csv"]);
594        assert_eq!(schema["type"], "string");
595        assert_eq!(schema["description"], "Format type");
596        let enum_vals = schema["enum"].as_array().unwrap();
597        assert_eq!(enum_vals.len(), 3);
598    }
599
600    #[test]
601    fn test_validation_args() {
602        let (strict, lint) = validation_args!();
603        assert_eq!(strict["type"], "boolean");
604        assert_eq!(strict["default"], true);
605        assert_eq!(lint["type"], "boolean");
606        assert_eq!(lint["default"], true);
607    }
608
609    #[test]
610    fn test_pagination_args() {
611        let (limit, offset) = pagination_args!();
612        assert_eq!(limit["type"], "integer");
613        assert_eq!(limit["default"], 100);
614        assert_eq!(offset["type"], "integer");
615        assert_eq!(offset["default"], 0);
616
617        let (custom_limit, _) = pagination_args!(default_limit: 50);
618        assert_eq!(custom_limit["default"], 50);
619    }
620
621    #[test]
622    fn test_file_write_args() {
623        let (validate, format, backup) = file_write_args!();
624        assert_eq!(validate["default"], true);
625        assert_eq!(format["default"], false);
626        assert_eq!(backup["default"], true);
627    }
628
629    #[test]
630    fn test_ditto_arg() {
631        let schema = ditto_arg!();
632        assert_eq!(schema["type"], "boolean");
633        assert_eq!(schema["default"], true);
634        assert!(schema["description"].as_str().unwrap().contains("ditto"));
635    }
636
637    #[test]
638    fn test_convert_to_options() {
639        let schema = convert_to_options!();
640        assert_eq!(schema["type"], "object");
641        assert!(schema["properties"].get("pretty").is_some());
642        assert!(schema["properties"].get("include_headers").is_some());
643        assert!(schema["properties"].get("use_merge").is_some());
644        assert!(schema["properties"].get("include_constraints").is_some());
645    }
646
647    #[test]
648    fn test_convert_from_options() {
649        let schema = convert_from_options!();
650        assert_eq!(schema["type"], "object");
651        assert!(schema["properties"].get("type_name").is_some());
652        assert!(schema["properties"].get("schema").is_some());
653        assert!(schema["properties"].get("delimiter").is_some());
654    }
655}