herolib_derive/lib.rs
1//! # herolib-derive: Derive Macros for herolib
2//!
3//! This crate provides procedural macros for herolib:
4//!
5//! ## Schema & Serialization Macros
6//!
7//! - **ToSchema** - Generate JSON Schema from Rust structs
8//! - **ToHeroScript** - Serialize Rust structs to HeroScript format
9//! - **FromHeroScript** - Deserialize HeroScript into Rust structs
10//!
11//! ## MCP Client Macros
12//!
13//! - **mcp_client_from_json** - Generate typed MCP client from inline JSON spec
14//! - **mcp_client_from_file** - Generate typed MCP client from JSON file
15//! - **mcp_tool** - Mark functions as MCP tools
16//!
17//! ## OpenRPC Client Macros
18//!
19//! - **openrpc_client!** - Generate typed RPC client from OpenRPC specification
20//!
21//! ## ToSchema Usage
22//!
23//! ```ignore
24//! use herolib_derive::ToSchema;
25//!
26//! #[derive(ToSchema)]
27//! struct Inner {
28//! value: i32,
29//! }
30//!
31//! #[derive(ToSchema)]
32//! struct Outer {
33//! name: String,
34//! inner: Inner,
35//! }
36//!
37//! let schema = Outer::json_schema_pretty();
38//! println!("{}", schema);
39//! ```
40//!
41//! ## HeroScript Usage
42//!
43//! ```ignore
44//! use herolib_derive::{ToHeroScript, FromHeroScript};
45//!
46//! #[derive(ToHeroScript, FromHeroScript, Default)]
47//! struct Person {
48//! name: String,
49//! age: u32,
50//! active: bool,
51//! }
52//!
53//! // Serialize to HeroScript
54//! let person = Person { name: "John".into(), age: 30, active: true };
55//! let hs = person.to_heroscript("person", "define");
56//!
57//! // Deserialize from HeroScript
58//! let script = "!!person.define name:Jane age:25 active:false";
59//! let jane = Person::from_heroscript(script).unwrap();
60//! ```
61//!
62//! ## OTOML Usage
63//!
64//! ```ignore
65//! use herolib_derive::Otoml;
66//! use serde::{Serialize, Deserialize};
67//!
68//! #[derive(Otoml, Serialize, Deserialize)]
69//! struct Config {
70//! name: String,
71//! port: u32,
72//! debug: bool,
73//! }
74//!
75//! // Serialize to canonical OTOML
76//! let config = Config { name: "server".into(), port: 8080, debug: true };
77//! let otoml = config.dump_otoml().unwrap();
78//!
79//! // Deserialize from any valid TOML
80//! let parsed = Config::load_otoml(&otoml).unwrap();
81//! ```
82
83use proc_macro::TokenStream;
84use proc_macro2::TokenStream as TokenStream2;
85use quote::quote;
86use syn::{
87 Data, DataEnum, DeriveInput, Fields, GenericArgument, Ident, PathArguments, Type,
88 parse_macro_input,
89};
90
91mod heroscript;
92mod mcp;
93mod openrpc_client;
94mod osis_object;
95
96// ============================================================================
97// Schema Derive Macro
98// ============================================================================
99
100/// Derive macro for generating JSON Schema from structs.
101///
102/// This macro adds these methods to the struct:
103/// - `json_schema()` - Returns compact JSON Schema with `$schema` header
104/// - `json_schema_pretty()` - Returns formatted JSON Schema
105/// - `schema_object()` - Returns just the object schema (for embedding)
106///
107/// # Example
108///
109/// ```ignore
110/// use herolib_derive::ToSchema;
111///
112/// #[derive(ToSchema)]
113/// struct Person {
114/// name: String,
115/// age: u32,
116/// active: bool,
117/// }
118///
119/// let schema = Person::json_schema_pretty();
120/// ```
121#[proc_macro_derive(ToSchema)]
122pub fn to_schema_derive(input: TokenStream) -> TokenStream {
123 let input = parse_macro_input!(input as DeriveInput);
124 let name = &input.ident;
125 let name_str = name.to_string();
126
127 let schema_impl = match &input.data {
128 Data::Struct(data_struct) => generate_struct_schema(name, &name_str, &data_struct.fields),
129 Data::Enum(data_enum) => generate_enum_schema(name, &name_str, data_enum),
130 Data::Union(_) => {
131 return syn::Error::new_spanned(&input, "ToSchema does not support unions")
132 .to_compile_error()
133 .into();
134 }
135 };
136
137 TokenStream::from(schema_impl)
138}
139
140fn generate_struct_schema(name: &Ident, name_str: &str, fields: &Fields) -> TokenStream2 {
141 let fields_named = match fields {
142 Fields::Named(fields) => &fields.named,
143 Fields::Unnamed(_) => {
144 return quote! {
145 impl #name {
146 pub fn schema_object() -> String {
147 r#"{"type":"array"}"#.to_string()
148 }
149
150 pub fn json_schema() -> String {
151 format!(
152 r#"{{"$schema":"http://json-schema.org/draft-07/schema#","title":"{}","type":"array"}}"#,
153 #name_str
154 )
155 }
156
157 pub fn json_schema_pretty() -> String {
158 Self::pretty_print_json(&Self::json_schema())
159 }
160
161 fn pretty_print_json(json: &str) -> String {
162 let mut result = String::new();
163 let mut indent = 0;
164 let mut in_string = false;
165 let mut prev_char = ' ';
166
167 for ch in json.chars() {
168 if ch == '"' && prev_char != '\\' {
169 in_string = !in_string;
170 }
171
172 if in_string {
173 result.push(ch);
174 } else {
175 match ch {
176 '{' | '[' => {
177 result.push(ch);
178 result.push('\n');
179 indent += 2;
180 result.push_str(&" ".repeat(indent));
181 }
182 '}' | ']' => {
183 result.push('\n');
184 indent = indent.saturating_sub(2);
185 result.push_str(&" ".repeat(indent));
186 result.push(ch);
187 }
188 ',' => {
189 result.push(ch);
190 result.push('\n');
191 result.push_str(&" ".repeat(indent));
192 }
193 ':' => {
194 result.push(ch);
195 result.push(' ');
196 }
197 ' ' | '\n' | '\t' | '\r' => {}
198 _ => result.push(ch),
199 }
200 }
201 prev_char = ch;
202 }
203 result
204 }
205 }
206 };
207 }
208 Fields::Unit => {
209 return quote! {
210 impl #name {
211 pub fn schema_object() -> String {
212 r#"{"type":"null"}"#.to_string()
213 }
214
215 pub fn json_schema() -> String {
216 format!(
217 r#"{{"$schema":"http://json-schema.org/draft-07/schema#","title":"{}","type":"null"}}"#,
218 #name_str
219 )
220 }
221
222 pub fn json_schema_pretty() -> String {
223 Self::pretty_print_json(&Self::json_schema())
224 }
225
226 fn pretty_print_json(json: &str) -> String {
227 let mut result = String::new();
228 let mut indent = 0;
229 let mut in_string = false;
230 let mut prev_char = ' ';
231
232 for ch in json.chars() {
233 if ch == '"' && prev_char != '\\' {
234 in_string = !in_string;
235 }
236
237 if in_string {
238 result.push(ch);
239 } else {
240 match ch {
241 '{' | '[' => {
242 result.push(ch);
243 result.push('\n');
244 indent += 2;
245 result.push_str(&" ".repeat(indent));
246 }
247 '}' | ']' => {
248 result.push('\n');
249 indent = indent.saturating_sub(2);
250 result.push_str(&" ".repeat(indent));
251 result.push(ch);
252 }
253 ',' => {
254 result.push(ch);
255 result.push('\n');
256 result.push_str(&" ".repeat(indent));
257 }
258 ':' => {
259 result.push(ch);
260 result.push(' ');
261 }
262 ' ' | '\n' | '\t' | '\r' => {}
263 _ => result.push(ch),
264 }
265 }
266 prev_char = ch;
267 }
268 result
269 }
270 }
271 };
272 }
273 };
274
275 // Collect field info for code generation
276 let mut field_names: Vec<String> = Vec::new();
277 let mut field_schema_exprs: Vec<TokenStream2> = Vec::new();
278 let mut required_field_names: Vec<String> = Vec::new();
279
280 for field in fields_named.iter() {
281 let field_name = field.ident.as_ref().unwrap().to_string();
282 let (schema_expr, is_optional) = type_to_schema_expr(&field.ty);
283
284 field_names.push(field_name.clone());
285 field_schema_exprs.push(schema_expr);
286
287 if !is_optional {
288 required_field_names.push(field_name);
289 }
290 }
291
292 // Generate the properties building code
293 let property_builders: Vec<TokenStream2> = field_names
294 .iter()
295 .zip(field_schema_exprs.iter())
296 .map(|(name, expr)| {
297 quote! {
298 props.push(format!(r#""{}": {}"#, #name, #expr));
299 }
300 })
301 .collect();
302
303 // Generate required array
304 let required_literals: Vec<TokenStream2> = required_field_names
305 .iter()
306 .map(|name| quote! { format!(r#""{}""#, #name) })
307 .collect();
308
309 quote! {
310 impl #name {
311 /// Returns the schema object for this type (without $schema header).
312 /// Use this when embedding this type's schema in another schema.
313 pub fn schema_object() -> String {
314 let mut props: Vec<String> = Vec::new();
315 #(#property_builders)*
316
317 let required: Vec<String> = vec![#(#required_literals),*];
318
319 format!(
320 r#"{{"type": "object", "properties": {{{}}}, "required": [{}]}}"#,
321 props.join(", "),
322 required.join(", ")
323 )
324 }
325
326 /// Returns a JSON Schema string for this type with full $schema header.
327 pub fn json_schema() -> String {
328 let mut props: Vec<String> = Vec::new();
329 #(#property_builders)*
330
331 let required: Vec<String> = vec![#(#required_literals),*];
332
333 format!(
334 r#"{{"$schema": "http://json-schema.org/draft-07/schema#", "title": "{}", "type": "object", "properties": {{{}}}, "required": [{}]}}"#,
335 #name_str,
336 props.join(", "),
337 required.join(", ")
338 )
339 }
340
341 /// Returns a pretty-printed JSON Schema string for this type.
342 pub fn json_schema_pretty() -> String {
343 Self::pretty_print_json(&Self::json_schema())
344 }
345
346 fn pretty_print_json(json: &str) -> String {
347 let mut result = String::new();
348 let mut indent = 0;
349 let mut in_string = false;
350 let mut prev_char = ' ';
351
352 for ch in json.chars() {
353 if ch == '"' && prev_char != '\\' {
354 in_string = !in_string;
355 }
356
357 if in_string {
358 result.push(ch);
359 } else {
360 match ch {
361 '{' | '[' => {
362 result.push(ch);
363 result.push('\n');
364 indent += 2;
365 result.push_str(&" ".repeat(indent));
366 }
367 '}' | ']' => {
368 result.push('\n');
369 indent = indent.saturating_sub(2);
370 result.push_str(&" ".repeat(indent));
371 result.push(ch);
372 }
373 ',' => {
374 result.push(ch);
375 result.push('\n');
376 result.push_str(&" ".repeat(indent));
377 }
378 ':' => {
379 result.push(ch);
380 result.push(' ');
381 }
382 ' ' | '\n' | '\t' | '\r' => {}
383 _ => result.push(ch),
384 }
385 }
386 prev_char = ch;
387 }
388 result
389 }
390 }
391 }
392}
393
394/// Generate JSON Schema for an enum type.
395fn generate_enum_schema(name: &Ident, name_str: &str, data_enum: &DataEnum) -> TokenStream2 {
396 // Check if all variants are unit variants (simple enum)
397 let all_unit_variants = data_enum
398 .variants
399 .iter()
400 .all(|v| matches!(v.fields, Fields::Unit));
401
402 if all_unit_variants {
403 // Simple enum with only unit variants - use JSON Schema enum
404 let variant_names: Vec<String> = data_enum
405 .variants
406 .iter()
407 .map(|v| v.ident.to_string())
408 .collect();
409
410 let variant_literals: Vec<TokenStream2> = variant_names
411 .iter()
412 .map(|name| quote! { format!(r#""{}""#, #name) })
413 .collect();
414
415 quote! {
416 impl #name {
417 pub fn schema_object() -> String {
418 let variants: Vec<String> = vec![#(#variant_literals),*];
419 format!(r#"{{"enum": [{}]}}"#, variants.join(", "))
420 }
421
422 pub fn json_schema() -> String {
423 let variants: Vec<String> = vec![#(#variant_literals),*];
424 format!(
425 r#"{{"$schema": "http://json-schema.org/draft-07/schema#", "title": "{}", "enum": [{}]}}"#,
426 #name_str,
427 variants.join(", ")
428 )
429 }
430
431 pub fn json_schema_pretty() -> String {
432 Self::pretty_print_json(&Self::json_schema())
433 }
434
435 fn pretty_print_json(json: &str) -> String {
436 let mut result = String::new();
437 let mut indent = 0;
438 let mut in_string = false;
439 let mut prev_char = ' ';
440
441 for ch in json.chars() {
442 if ch == '"' && prev_char != '\\' {
443 in_string = !in_string;
444 }
445
446 if in_string {
447 result.push(ch);
448 } else {
449 match ch {
450 '{' | '[' => {
451 result.push(ch);
452 result.push('\n');
453 indent += 2;
454 result.push_str(&" ".repeat(indent));
455 }
456 '}' | ']' => {
457 result.push('\n');
458 indent = indent.saturating_sub(2);
459 result.push_str(&" ".repeat(indent));
460 result.push(ch);
461 }
462 ',' => {
463 result.push(ch);
464 result.push('\n');
465 result.push_str(&" ".repeat(indent));
466 }
467 ':' => {
468 result.push(ch);
469 result.push(' ');
470 }
471 ' ' | '\n' | '\t' | '\r' => {}
472 _ => result.push(ch),
473 }
474 }
475 prev_char = ch;
476 }
477 result
478 }
479 }
480 }
481 } else {
482 // Complex enum with data variants - use oneOf
483 let mut variant_schema_builders: Vec<TokenStream2> = Vec::new();
484
485 for variant in &data_enum.variants {
486 let variant_name = variant.ident.to_string();
487
488 match &variant.fields {
489 Fields::Unit => {
490 variant_schema_builders.push(quote! {
491 schemas.push(format!(r#"{{"const": "{}"}}"#, #variant_name));
492 });
493 }
494 Fields::Unnamed(fields) => {
495 if fields.unnamed.len() == 1 {
496 let inner_ty = &fields.unnamed.first().unwrap().ty;
497 let (inner_expr, _) = type_to_schema_expr(inner_ty);
498 variant_schema_builders.push(quote! {
499 schemas.push(format!(
500 r#"{{"type": "object", "properties": {{"{}": {}}}, "required": ["{}"]}}"#,
501 #variant_name,
502 #inner_expr,
503 #variant_name
504 ));
505 });
506 } else {
507 let item_exprs: Vec<TokenStream2> = fields
508 .unnamed
509 .iter()
510 .map(|f| {
511 let (expr, _) = type_to_schema_expr(&f.ty);
512 expr
513 })
514 .collect();
515 let len = item_exprs.len();
516 variant_schema_builders.push(quote! {
517 let items: Vec<String> = vec![#(#item_exprs),*];
518 schemas.push(format!(
519 r#"{{"type": "object", "properties": {{"{}": {{"type": "array", "items": [{}], "minItems": {}, "maxItems": {}}}}}, "required": ["{}"]}}"#,
520 #variant_name,
521 items.join(", "),
522 #len,
523 #len,
524 #variant_name
525 ));
526 });
527 }
528 }
529 Fields::Named(fields) => {
530 let field_builders: Vec<TokenStream2> = fields
531 .named
532 .iter()
533 .map(|f| {
534 let field_name = f.ident.as_ref().unwrap().to_string();
535 let (field_expr, _) = type_to_schema_expr(&f.ty);
536 quote! {
537 inner_props.push(format!(r#""{}": {}"#, #field_name, #field_expr));
538 }
539 })
540 .collect();
541
542 let required_fields: Vec<TokenStream2> = fields
543 .named
544 .iter()
545 .filter_map(|f| {
546 let (_, is_optional) = type_to_schema_expr(&f.ty);
547 if !is_optional {
548 let field_name = f.ident.as_ref().unwrap().to_string();
549 Some(quote! { format!(r#""{}""#, #field_name) })
550 } else {
551 None
552 }
553 })
554 .collect();
555
556 variant_schema_builders.push(quote! {
557 {
558 let mut inner_props: Vec<String> = Vec::new();
559 #(#field_builders)*
560 let required: Vec<String> = vec![#(#required_fields),*];
561 schemas.push(format!(
562 r#"{{"type": "object", "properties": {{"{}": {{"type": "object", "properties": {{{}}}, "required": [{}]}}}}, "required": ["{}"]}}"#,
563 #variant_name,
564 inner_props.join(", "),
565 required.join(", "),
566 #variant_name
567 ));
568 }
569 });
570 }
571 }
572 }
573
574 quote! {
575 impl #name {
576 pub fn schema_object() -> String {
577 let mut schemas: Vec<String> = Vec::new();
578 #(#variant_schema_builders)*
579 format!(r#"{{"oneOf": [{}]}}"#, schemas.join(", "))
580 }
581
582 pub fn json_schema() -> String {
583 let mut schemas: Vec<String> = Vec::new();
584 #(#variant_schema_builders)*
585 format!(
586 r#"{{"$schema": "http://json-schema.org/draft-07/schema#", "title": "{}", "oneOf": [{}]}}"#,
587 #name_str,
588 schemas.join(", ")
589 )
590 }
591
592 pub fn json_schema_pretty() -> String {
593 Self::pretty_print_json(&Self::json_schema())
594 }
595
596 fn pretty_print_json(json: &str) -> String {
597 let mut result = String::new();
598 let mut indent = 0;
599 let mut in_string = false;
600 let mut prev_char = ' ';
601
602 for ch in json.chars() {
603 if ch == '"' && prev_char != '\\' {
604 in_string = !in_string;
605 }
606
607 if in_string {
608 result.push(ch);
609 } else {
610 match ch {
611 '{' | '[' => {
612 result.push(ch);
613 result.push('\n');
614 indent += 2;
615 result.push_str(&" ".repeat(indent));
616 }
617 '}' | ']' => {
618 result.push('\n');
619 indent = indent.saturating_sub(2);
620 result.push_str(&" ".repeat(indent));
621 result.push(ch);
622 }
623 ',' => {
624 result.push(ch);
625 result.push('\n');
626 result.push_str(&" ".repeat(indent));
627 }
628 ':' => {
629 result.push(ch);
630 result.push(' ');
631 }
632 ' ' | '\n' | '\t' | '\r' => {}
633 _ => result.push(ch),
634 }
635 }
636 prev_char = ch;
637 }
638 result
639 }
640 }
641 }
642 }
643}
644
645/// Convert a Rust type to a TokenStream expression that produces the schema string at runtime.
646fn type_to_schema_expr(ty: &Type) -> (TokenStream2, bool) {
647 match ty {
648 Type::Path(type_path) => {
649 let segments = &type_path.path.segments;
650 if let Some(segment) = segments.last() {
651 let type_name = segment.ident.to_string();
652
653 match type_name.as_str() {
654 "String" | "str" => (quote! { r#"{"type": "string"}"#.to_string() }, false),
655 "bool" => (quote! { r#"{"type": "boolean"}"#.to_string() }, false),
656 "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32"
657 | "u64" | "u128" | "usize" => {
658 (quote! { r#"{"type": "integer"}"#.to_string() }, false)
659 }
660 "f32" | "f64" => (quote! { r#"{"type": "number"}"#.to_string() }, false),
661 "Option" => {
662 if let PathArguments::AngleBracketed(args) = &segment.arguments {
663 if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
664 let (inner_expr, _) = type_to_schema_expr(inner_ty);
665 return (
666 quote! {
667 {
668 let inner = #inner_expr;
669 if inner.contains(r#""type": "string""#) {
670 r#"{"type": ["string", "null"]}"#.to_string()
671 } else if inner.contains(r#""type": "integer""#) {
672 r#"{"type": ["integer", "null"]}"#.to_string()
673 } else if inner.contains(r#""type": "number""#) {
674 r#"{"type": ["number", "null"]}"#.to_string()
675 } else if inner.contains(r#""type": "boolean""#) {
676 r#"{"type": ["boolean", "null"]}"#.to_string()
677 } else if inner.contains(r#""type": "array""#) {
678 format!(r#"{{"oneOf": [{}, {{"type": "null"}}]}}"#, inner)
679 } else if inner.contains(r#""type": "object""#) {
680 format!(r#"{{"oneOf": [{}, {{"type": "null"}}]}}"#, inner)
681 } else {
682 format!(r#"{{"oneOf": [{}, {{"type": "null"}}]}}"#, inner)
683 }
684 }
685 },
686 true,
687 );
688 }
689 }
690 (quote! { r#"{"type": "null"}"#.to_string() }, true)
691 }
692 "Vec" => {
693 if let PathArguments::AngleBracketed(args) = &segment.arguments {
694 if let Some(GenericArgument::Type(inner_ty)) = args.args.first() {
695 let (inner_expr, _) = type_to_schema_expr(inner_ty);
696 return (
697 quote! {
698 format!(r#"{{"type": "array", "items": {}}}"#, #inner_expr)
699 },
700 false,
701 );
702 }
703 }
704 (quote! { r#"{"type": "array"}"#.to_string() }, false)
705 }
706 "HashMap" | "BTreeMap" => {
707 if let PathArguments::AngleBracketed(args) = &segment.arguments {
708 let mut args_iter = args.args.iter();
709 let _ = args_iter.next();
710 if let Some(GenericArgument::Type(value_ty)) = args_iter.next() {
711 let (value_expr, _) = type_to_schema_expr(value_ty);
712 return (
713 quote! {
714 format!(r#"{{"type": "object", "additionalProperties": {}}}"#, #value_expr)
715 },
716 false,
717 );
718 }
719 }
720 (quote! { r#"{"type": "object"}"#.to_string() }, false)
721 }
722 _ => {
723 let type_ident = &segment.ident;
724 (quote! { #type_ident::schema_object() }, false)
725 }
726 }
727 } else {
728 (quote! { r#"{"type": "object"}"#.to_string() }, false)
729 }
730 }
731 Type::Reference(type_ref) => type_to_schema_expr(&type_ref.elem),
732 Type::Slice(type_slice) => {
733 let (inner_expr, _) = type_to_schema_expr(&type_slice.elem);
734 (
735 quote! { format!(r#"{{"type": "array", "items": {}}}"#, #inner_expr) },
736 false,
737 )
738 }
739 Type::Array(type_array) => {
740 let (inner_expr, _) = type_to_schema_expr(&type_array.elem);
741 (
742 quote! { format!(r#"{{"type": "array", "items": {}}}"#, #inner_expr) },
743 false,
744 )
745 }
746 Type::Tuple(type_tuple) => {
747 let item_exprs: Vec<TokenStream2> = type_tuple
748 .elems
749 .iter()
750 .map(|t| {
751 let (expr, _) = type_to_schema_expr(t);
752 expr
753 })
754 .collect();
755 let len = item_exprs.len();
756 (
757 quote! {
758 {
759 let items: Vec<String> = vec![#(#item_exprs),*];
760 format!(
761 r#"{{"type": "array", "items": [{}], "minItems": {}, "maxItems": {}}}"#,
762 items.join(", "),
763 #len,
764 #len
765 )
766 }
767 },
768 false,
769 )
770 }
771 _ => (quote! { r#"{"type": "object"}"#.to_string() }, false),
772 }
773}
774
775// ============================================================================
776// HeroScript Derive Macros
777// ============================================================================
778
779/// Derive macro for serializing structs to HeroScript format.
780#[proc_macro_derive(ToHeroScript)]
781pub fn to_heroscript_derive(input: TokenStream) -> TokenStream {
782 heroscript::impl_to_heroscript(input)
783}
784
785/// Derive macro for deserializing HeroScript into structs.
786#[proc_macro_derive(FromHeroScript)]
787pub fn from_heroscript_derive(input: TokenStream) -> TokenStream {
788 heroscript::impl_from_heroscript(input)
789}
790
791// ============================================================================
792// OTOML Derive Macro
793// ============================================================================
794
795/// Derive macro for OTOML serialization/deserialization.
796///
797/// This macro adds two methods to structs that implement `Serialize` and `Deserialize`:
798/// - `dump_otoml()` - Serialize to canonical OTOML format (deterministic, sorted keys)
799/// - `load_otoml(s)` - Deserialize from any valid TOML string
800///
801/// # Example
802///
803/// ```ignore
804/// use herolib_derive::Otoml;
805/// use serde::{Serialize, Deserialize};
806///
807/// #[derive(Debug, Serialize, Deserialize, Otoml)]
808/// struct Config {
809/// name: String,
810/// port: u32,
811/// debug: bool,
812/// }
813///
814/// let config = Config { name: "server".into(), port: 8080, debug: true };
815///
816/// // Serialize to canonical OTOML
817/// let otoml = config.dump_otoml().unwrap();
818/// println!("{}", otoml);
819/// // Output:
820/// // debug = true
821/// // name = "server"
822/// // port = 8080
823///
824/// // Deserialize from TOML
825/// let parsed = Config::load_otoml(&otoml).unwrap();
826/// ```
827///
828/// # Requirements
829///
830/// The struct must also derive `Serialize` and `Deserialize` from serde.
831#[proc_macro_derive(Otoml)]
832pub fn otoml_derive(input: TokenStream) -> TokenStream {
833 let input = parse_macro_input!(input as DeriveInput);
834 let name = &input.ident;
835
836 let expanded = quote! {
837 impl #name {
838 /// Serializes this value to canonical OTOML format.
839 ///
840 /// OTOML is a deterministic subset of TOML with:
841 /// - Sorted keys (alphabetically)
842 /// - Inline tables for nested structures
843 /// - Consistent formatting
844 /// - "O:\n" prefix header
845 pub fn dump_otoml(&self) -> Result<String, herolib_osis::OtomlError> {
846 herolib_osis::dump_otoml(self)
847 }
848
849 /// Deserializes from an OTOML string.
850 ///
851 /// The input should start with "O:\n" prefix.
852 pub fn load_otoml(s: &str) -> Result<Self, herolib_osis::OtomlError> {
853 herolib_osis::load_otoml(s)
854 }
855 }
856 };
857
858 TokenStream::from(expanded)
859}
860
861// ============================================================================
862// MCP Client Macros
863// ============================================================================
864
865#[proc_macro]
866pub fn mcp_client_from_json(input: TokenStream) -> TokenStream {
867 mcp::impl_mcp_client_from_json(input)
868}
869
870#[proc_macro]
871pub fn mcp_client_from_file(input: TokenStream) -> TokenStream {
872 mcp::impl_mcp_client_from_file(input)
873}
874
875#[proc_macro_attribute]
876pub fn mcp_tool(attr: TokenStream, item: TokenStream) -> TokenStream {
877 mcp::impl_mcp_tool(attr, item)
878}
879
880// ============================================================================
881// OpenRPC Client Macros
882// ============================================================================
883
884/// Generate a typed RPC client from an OpenRPC specification.
885///
886/// This macro reads an OpenRPC specification and generates a strongly-typed
887/// Rust client with methods for each RPC endpoint.
888///
889/// # Usage
890///
891/// ## From file path
892/// ```ignore
893/// use herolib_derive::openrpc_client;
894///
895/// // Generate client from OpenRPC spec file (path relative to Cargo.toml)
896/// openrpc_client!("specs/myservice.openrpc.json");
897///
898/// // Use the generated client
899/// let client = MyServiceClient::connect().await?;
900/// let result = client.my_method(MyMethodInput { ... }).await?;
901/// ```
902///
903/// ## From Unix socket (with discover)
904/// ```ignore
905/// // Generate client that discovers spec from Unix socket at compile time
906/// openrpc_client!(socket = "/tmp/myservice.sock");
907/// ```
908///
909/// ## With custom client name
910/// ```ignore
911/// openrpc_client!("spec.json", name = "CustomClient");
912/// ```
913///
914/// # Generated Code
915///
916/// For each method in the OpenRPC spec, the macro generates:
917/// - Input struct with all parameters
918/// - Output struct with result fields
919/// - Async method on the client struct
920///
921/// The generated client supports:
922/// - HTTP transport (`connect_http(url)`)
923/// - Unix socket transport (`connect_socket(path)`)
924/// - Discovery (`client.discover()`)
925#[proc_macro]
926pub fn openrpc_client(input: TokenStream) -> TokenStream {
927 openrpc_client::impl_openrpc_client(input)
928}
929
930// ============================================================================
931// OsisObject Derive Macro
932// ============================================================================
933
934/// Derive macro for implementing `OsisObject` trait.
935///
936/// This macro generates the `OsisObject` trait implementation for structs
937/// that have a `sid: SmartId` field. The `type_name()` defaults to the
938/// struct name in snake_case, but can be overridden with an attribute.
939///
940/// # Attributes
941///
942/// - `#[osis(type_name = "name")]` - Override the default type name
943/// - `#[osis(index = "field1, field2")]` - Specify fields for full-text indexing
944///
945/// # Example
946///
947/// ```ignore
948/// use herolib_derive::OsisObject;
949/// use herolib_osis::sid::SmartId;
950/// use serde::{Serialize, Deserialize};
951///
952/// // Auto-generated type_name: "user"
953/// #[derive(Default, Serialize, Deserialize, OsisObject)]
954/// struct User {
955/// sid: SmartId,
956/// name: String,
957/// }
958///
959/// // Custom type_name override
960/// #[derive(Default, Serialize, Deserialize, OsisObject)]
961/// #[osis(type_name = "members")]
962/// struct Member {
963/// sid: SmartId,
964/// name: String,
965/// }
966///
967/// // With full-text indexing
968/// #[derive(Default, Serialize, Deserialize, OsisObject)]
969/// #[osis(index = "title, content")]
970/// struct Article {
971/// sid: SmartId,
972/// title: String,
973/// content: String,
974/// author: String, // Not indexed
975/// }
976///
977/// // Combined attributes
978/// #[derive(Default, Serialize, Deserialize, OsisObject)]
979/// #[osis(type_name = "articles", index = "title, content")]
980/// struct BlogPost {
981/// sid: SmartId,
982/// title: String,
983/// content: String,
984/// }
985/// ```
986///
987/// # Requirements
988///
989/// - The struct must have a field named `sid` of type `SmartId`
990/// - The struct should also derive `Serialize` and `Deserialize` from serde
991#[proc_macro_derive(OsisObject, attributes(osis))]
992pub fn osis_object_derive(input: TokenStream) -> TokenStream {
993 osis_object::impl_osis_object(input)
994}
995
996#[cfg(test)]
997mod tests {
998 // Tests are in a separate test file
999}