Skip to main content

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}