Skip to main content

sails_idl_ast/
lib.rs

1#![no_std]
2
3extern crate alloc;
4
5pub mod codec;
6mod hash;
7mod interface_id;
8
9use alloc::{
10    boxed::Box,
11    format,
12    string::{String, ToString as _},
13    vec,
14    vec::Vec,
15};
16use core::{
17    fmt::{Display, Write},
18    str::FromStr,
19};
20pub use interface_id::InterfaceId;
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24// -------------------------------- IDL model ---------------------------------
25pub type Annotation = (String, Option<String>);
26
27/// Root AST node representing a single parsed Sails IDL document.
28///
29/// Mirrors one `.idl` file from the specification:
30/// - `globals` correspond to global `!@...` annotations at the top of the file;
31/// - `program` holds an optional `program <ident> { ... }` block;
32/// - `services` contains all top-level `service <ident> { ... }` definitions.
33#[derive(Debug, Default, Clone, PartialEq)]
34#[cfg_attr(
35    feature = "templates",
36    derive(askama::Template),
37    template(path = "idl.askama", escape = "none")
38)]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40pub struct IdlDoc {
41    #[cfg_attr(
42        feature = "serde",
43        serde(default, skip_serializing_if = "Vec::is_empty")
44    )]
45    pub globals: Vec<Annotation>,
46    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
47    pub program: Option<ProgramUnit>,
48    #[cfg_attr(
49        feature = "serde",
50        serde(default, skip_serializing_if = "Vec::is_empty")
51    )]
52    pub services: Vec<ServiceUnit>,
53}
54
55impl IdlDoc {
56    #[cfg(feature = "serde")]
57    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
58        serde_json::to_string(self)
59    }
60
61    #[cfg(feature = "serde")]
62    pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
63        serde_json::to_string_pretty(self)
64    }
65}
66
67/// AST node describing a `program` block.
68///
69/// A program is an entry point that:
70/// - declares constructor functions in `constructors { ... }`,
71/// - exposes one or more services in `services { ... }`,
72/// - may define shared types in `types { ... }`,
73/// - may contain documentation comments and annotations.
74///
75/// Call [`ProgramUnit::normalize`] after construction to populate `entry_id` on each constructor.
76#[derive(Debug, Default, Clone, PartialEq)]
77#[cfg_attr(
78    feature = "templates",
79    derive(askama::Template),
80    template(path = "program.askama", escape = "none")
81)]
82#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
83pub struct ProgramUnit {
84    pub name: String,
85    #[cfg_attr(
86        feature = "serde",
87        serde(default, skip_serializing_if = "Vec::is_empty")
88    )]
89    pub ctors: Vec<CtorFunc>,
90    #[cfg_attr(
91        feature = "serde",
92        serde(default, skip_serializing_if = "Vec::is_empty")
93    )]
94    pub services: Vec<ServiceExpo>,
95    #[cfg_attr(
96        feature = "serde",
97        serde(default, skip_serializing_if = "Vec::is_empty")
98    )]
99    pub types: Vec<Type>,
100    #[cfg_attr(
101        feature = "serde",
102        serde(default, skip_serializing_if = "Vec::is_empty")
103    )]
104    pub docs: Vec<String>,
105    #[cfg_attr(
106        feature = "serde",
107        serde(default, skip_serializing_if = "Vec::is_empty")
108    )]
109    pub annotations: Vec<Annotation>,
110}
111
112impl ProgramUnit {
113    /// Compute `entry_id` for each constructor from `@entry_id` annotation,
114    /// falling back to declaration-order index.
115    /// Must be called after parsing, before any reordering.
116    pub fn normalize(&mut self) {
117        for (idx, ctor) in self.ctors.iter_mut().enumerate() {
118            ctor.entry_id = entry_id_from_annotations(&ctor.annotations, idx as u16);
119        }
120    }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
125pub struct ServiceIdent {
126    pub name: String,
127    #[cfg_attr(
128        feature = "serde",
129        serde(
130            default,
131            skip_serializing_if = "Option::is_none",
132            with = "serde_opt_str"
133        )
134    )]
135    pub interface_id: Option<InterfaceId>,
136}
137
138impl Display for ServiceIdent {
139    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
140        f.write_str(&self.name)?;
141        if let Some(id) = self.interface_id {
142            f.write_str("@")?;
143            id.fmt(f)?;
144        }
145        Ok(())
146    }
147}
148
149impl FromStr for ServiceIdent {
150    type Err = String;
151
152    fn from_str(s: &str) -> Result<Self, Self::Err> {
153        let (name, interface_id) = match s.split_once('@') {
154            None => (s.trim().to_string(), None),
155            Some((name, id_str)) => {
156                if name.is_empty() {
157                    return Err("name is empty".to_string());
158                }
159                if id_str.is_empty() {
160                    return Err("interface_id is empty".to_string());
161                }
162
163                // Delegate parsing to InterfaceId's FromStr
164                let id = id_str.trim().parse::<InterfaceId>()?;
165                (name.trim().to_string(), Some(id))
166            }
167        };
168        Ok(ServiceIdent { name, interface_id })
169    }
170}
171
172/// Single service export entry inside a `program { services { ... } }` section.
173///
174/// Represents a link between:
175/// - the exported service name visible to the client,
176/// - an optional low-level `route` (transport / path) used by the runtime,
177/// - may contain documentation comments and annotations.
178#[derive(Debug, Clone, PartialEq)]
179#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
180pub struct ServiceExpo {
181    #[cfg_attr(feature = "serde", serde(flatten))]
182    pub name: ServiceIdent,
183    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
184    pub route: Option<String>,
185    pub route_idx: u8,
186    #[cfg_attr(
187        feature = "serde",
188        serde(default, skip_serializing_if = "Vec::is_empty")
189    )]
190    pub docs: Vec<String>,
191    #[cfg_attr(
192        feature = "serde",
193        serde(default, skip_serializing_if = "Vec::is_empty")
194    )]
195    pub annotations: Vec<Annotation>,
196}
197
198/// Constructor function of a program, declared in `constructors { ... }`.
199///
200/// A constructor describes how to create or initialize a program instance:
201/// - `name` is the constructor identifier,
202/// - `params` are the IDL-level arguments,
203/// - `entry_id` is the on-chain entry identifier (computed from `@entry_id` annotation or declaration order),
204/// - may contain documentation comments and annotations.
205#[derive(Debug, Clone, PartialEq)]
206#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
207pub struct CtorFunc {
208    pub name: String,
209    #[cfg_attr(feature = "serde", serde(default))]
210    pub params: Vec<FuncParam>,
211    #[cfg_attr(
212        feature = "serde",
213        serde(default, skip_serializing_if = "Option::is_none")
214    )]
215    pub throws: Option<TypeDecl>,
216    #[cfg_attr(feature = "serde", serde(default))]
217    pub entry_id: u16,
218    #[cfg_attr(
219        feature = "serde",
220        serde(default, skip_serializing_if = "Vec::is_empty")
221    )]
222    pub docs: Vec<String>,
223    #[cfg_attr(
224        feature = "serde",
225        serde(default, skip_serializing_if = "Vec::is_empty")
226    )]
227    pub annotations: Vec<Annotation>,
228}
229
230/// AST node describing a `service` definition.
231///
232/// A service is an interface that:
233/// - may `extends` other services, inheriting their events, functions and types,
234/// - defines `funcs` in `functions { ... }`,
235/// - defines `events` in `events { ... }`,
236/// - defines service-local `types { ... }`,
237/// - may contain documentation comments and annotations.
238#[derive(Debug, Clone, PartialEq)]
239#[cfg_attr(
240    feature = "templates",
241    derive(askama::Template),
242    template(path = "service.askama", escape = "none")
243)]
244#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
245pub struct ServiceUnit {
246    #[cfg_attr(feature = "serde", serde(flatten))]
247    pub name: ServiceIdent,
248    #[cfg_attr(
249        feature = "serde",
250        serde(default, skip_serializing_if = "Vec::is_empty")
251    )]
252    pub extends: Vec<ServiceIdent>,
253    #[cfg_attr(
254        feature = "serde",
255        serde(default, skip_serializing_if = "Vec::is_empty")
256    )]
257    pub funcs: Vec<ServiceFunc>,
258    #[cfg_attr(
259        feature = "serde",
260        serde(default, skip_serializing_if = "Vec::is_empty")
261    )]
262    pub events: Vec<ServiceEvent>,
263    #[cfg_attr(
264        feature = "serde",
265        serde(default, skip_serializing_if = "Vec::is_empty")
266    )]
267    pub types: Vec<Type>,
268    #[cfg_attr(
269        feature = "serde",
270        serde(default, skip_serializing_if = "Vec::is_empty")
271    )]
272    pub docs: Vec<String>,
273    #[cfg_attr(
274        feature = "serde",
275        serde(default, skip_serializing_if = "Vec::is_empty")
276    )]
277    pub annotations: Vec<Annotation>,
278}
279
280impl ServiceUnit {
281    /// Stabilize ordering for deterministic output and comparisons.
282    ///
283    /// Also computes `entry_id` for each func and event from `@entry_id` annotation,
284    /// falling back to declaration-order index.
285    pub fn normalize(&mut self) {
286        self.events.sort_by_key(|e| e.name.to_lowercase());
287        self.funcs.sort_by_key(|f| f.name.to_lowercase());
288        self.extends.sort_by_key(|e| e.name.to_lowercase());
289        // Assign entry_id AFTER sort: use @entry_id annotation if present,
290        // otherwise the post-sort (alphabetical) index, which matches scale-codec ordering.
291        for (idx, func) in self.funcs.iter_mut().enumerate() {
292            func.entry_id = entry_id_from_annotations(&func.annotations, idx as u16);
293        }
294        for (idx, event) in self.events.iter_mut().enumerate() {
295            event.entry_id = entry_id_from_annotations(&event.annotations, idx as u16);
296        }
297    }
298
299    /// Returns `true` if the service is annotated with `@partial`.
300    ///
301    /// Partial services describe a subset of an existing on-chain service and require
302    /// explicit `@entry_id` annotations on all functions and events.
303    pub fn is_partial(&self) -> bool {
304        self.annotations.iter().any(|(k, _)| k == "partial")
305    }
306}
307
308fn entry_id_from_annotations(annotations: &[(String, Option<String>)], fallback: u16) -> u16 {
309    annotations
310        .iter()
311        .find(|(k, _)| k == "entry_id")
312        .and_then(|(_, v)| v.as_ref()?.parse::<u16>().ok())
313        .unwrap_or(fallback)
314}
315
316/// Service function entry inside `service { functions { ... } }`.
317///
318/// - `params` is the ordered list of IDL parameters;
319/// - `output` is the return type (use `PrimitiveType::Void` for `()` / no value);
320/// - `throws` is an optional error type after the `throws` keyword;
321/// - `is_query` marks read-only / query functions as defined by the spec;
322/// - `entry_id` is the on-chain entry identifier (computed from `@entry_id` annotation or declaration order);
323/// - may contain documentation comments and annotations.
324#[derive(Debug, Clone, PartialEq)]
325#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
326pub struct ServiceFunc {
327    pub name: String,
328    #[cfg_attr(feature = "serde", serde(default))]
329    pub params: Vec<FuncParam>,
330    pub output: TypeDecl,
331    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
332    pub throws: Option<TypeDecl>,
333    pub kind: FunctionKind,
334    #[cfg_attr(feature = "serde", serde(default))]
335    pub entry_id: u16,
336    #[cfg_attr(
337        feature = "serde",
338        serde(default, skip_serializing_if = "Vec::is_empty")
339    )]
340    pub docs: Vec<String>,
341    #[cfg_attr(
342        feature = "serde",
343        serde(default, skip_serializing_if = "Vec::is_empty")
344    )]
345    pub annotations: Vec<Annotation>,
346}
347
348/// Function kind based on mutability.
349#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
350#[cfg_attr(
351    feature = "serde",
352    derive(Serialize, Deserialize),
353    serde(rename_all = "lowercase")
354)]
355pub enum FunctionKind {
356    #[default]
357    Command,
358    Query,
359}
360
361impl ServiceFunc {
362    /// Returns `true` if the function is declared with a `()` return type,
363    /// i.e. it does not produce a value on success.
364    pub fn returns_void(&self) -> bool {
365        use PrimitiveType::*;
366        use TypeDecl::*;
367        self.output == Primitive(Void)
368    }
369}
370
371/// Function parameter used in constructors and service functions.
372///
373/// Stores the parameter name as written in IDL and its fully resolved type
374/// (`TypeDecl`), preserving declaration order.
375#[derive(Debug, Clone, PartialEq, Eq)]
376#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
377pub struct FuncParam {
378    pub name: String,
379    #[cfg_attr(feature = "serde", serde(rename = "type"))]
380    pub type_decl: TypeDecl,
381}
382
383impl Display for FuncParam {
384    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
385        let FuncParam { name, type_decl } = self;
386        write!(f, "{name}: {type_decl}")
387    }
388}
389
390/// Service event is represented as an enum variant with an associated payload.
391///
392/// Events in `events { ... }` are modeled as `EnumVariant` with a `StructDef`
393/// describing fields of the event, so the same machinery as for enums can be reused.
394pub type ServiceEvent = EnumVariant;
395
396/// Generalized type descriptor used throughout the AST.
397///
398/// Covers all kinds of IDL types:
399/// - primitive types (`Primitive`),
400/// - slices and fixed arrays (`Slice`, `Array`),
401/// - tuples (`Tuple`),
402/// - named types (e.g. `Point<u32>`)
403///     - container types like `Option<T>`, `Result<T, E>`
404///     - user-defined types with generics (`UserDefined`),
405/// - generic type parameters (e.g. `T`).
406#[derive(Debug, Clone, PartialEq, Eq, Hash)]
407#[cfg_attr(
408    feature = "serde",
409    derive(Serialize, Deserialize),
410    serde(rename_all = "lowercase", tag = "kind")
411)]
412pub enum TypeDecl {
413    /// Slice type `[T]`.
414    Slice { item: Box<TypeDecl> },
415    /// Fixed-length array type `[T; N]`.
416    Array { item: Box<TypeDecl>, len: u32 },
417    /// Tuple type `(T1, T2, ...)`, including `()` for an empty tuple.
418    Tuple { types: Vec<TypeDecl> },
419    /// Generic type parameter reference declared by the surrounding [`Type`].
420    Generic { name: String },
421    /// Named type, possibly generic (e.g. `Point<u32>`).
422    Named {
423        name: String,
424        #[cfg_attr(
425            feature = "serde",
426            serde(default, skip_serializing_if = "Vec::is_empty")
427        )]
428        generics: Vec<TypeDecl>,
429    },
430    /// Built-in primitive type from `PrimitiveType`.
431    #[cfg_attr(feature = "serde", serde(untagged))]
432    Primitive(#[cfg_attr(feature = "serde", serde(with = "serde_str"))] PrimitiveType),
433}
434
435impl TypeDecl {
436    pub fn named(name: impl Into<String>) -> TypeDecl {
437        Self::named_with_generics(name, vec![])
438    }
439
440    pub fn named_with_generics(name: impl Into<String>, generics: Vec<TypeDecl>) -> TypeDecl {
441        TypeDecl::Named {
442            name: name.into(),
443            generics,
444        }
445    }
446
447    pub fn generic(name: impl Into<String>) -> TypeDecl {
448        TypeDecl::Generic { name: name.into() }
449    }
450
451    pub fn tuple(types: Vec<TypeDecl>) -> TypeDecl {
452        TypeDecl::Tuple { types }
453    }
454
455    pub fn option(item: TypeDecl) -> TypeDecl {
456        TypeDecl::Named {
457            name: "Option".to_string(),
458            generics: vec![item],
459        }
460    }
461
462    pub fn result(ok: TypeDecl, err: TypeDecl) -> TypeDecl {
463        TypeDecl::Named {
464            name: "Result".to_string(),
465            generics: vec![ok, err],
466        }
467    }
468
469    pub fn option_type_decl(ty: &TypeDecl) -> Option<TypeDecl> {
470        match ty {
471            TypeDecl::Named { name, generics } if name == "Option" => {
472                if let [item] = generics.as_slice() {
473                    Some(item.clone())
474                } else {
475                    None
476                }
477            }
478            _ => None,
479        }
480    }
481
482    pub fn result_type_decl(ty: &TypeDecl) -> Option<(TypeDecl, TypeDecl)> {
483        match ty {
484            TypeDecl::Named { name, generics } if name == "Result" => {
485                if let [ok, err] = generics.as_slice() {
486                    Some((ok.clone(), err.clone()))
487                } else {
488                    None
489                }
490            }
491            _ => None,
492        }
493    }
494
495    #[cfg(feature = "serde")]
496    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
497        serde_json::to_string(self)
498    }
499
500    #[cfg(feature = "serde")]
501    pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
502        serde_json::to_string_pretty(self)
503    }
504}
505
506impl Display for TypeDecl {
507    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
508        use TypeDecl::*;
509        match self {
510            Slice { item } => write!(f, "[{item}]"),
511            Array { item, len } => write!(f, "[{item}; {len}]"),
512            Tuple { types } => {
513                f.write_char('(')?;
514                for (i, ty) in types.iter().enumerate() {
515                    if i > 0 {
516                        f.write_str(", ")?;
517                    }
518                    write!(f, "{ty}")?;
519                }
520                f.write_char(')')?;
521                Ok(())
522            }
523            Generic { name } => f.write_str(name),
524            Named { name, generics } => {
525                write!(f, "{name}")?;
526                if !generics.is_empty() {
527                    f.write_char('<')?;
528                    for (i, g) in generics.iter().enumerate() {
529                        if i > 0 {
530                            f.write_str(", ")?;
531                        }
532                        write!(f, "{g}")?;
533                    }
534                    f.write_char('>')?;
535                }
536                Ok(())
537            }
538            Primitive(primitive_type) => write!(f, "{primitive_type}"),
539        }
540    }
541}
542
543/// Enumeration of all built-in primitive types supported by the IDL.
544///
545/// Includes booleans, characters, signed/unsigned integers, string, and
546/// platform-specific identifiers and hashes (ActorId, CodeId, MessageId,
547/// H160/H256/U256) used by the runtime.
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
549#[repr(u8)]
550pub enum PrimitiveType {
551    /// Unit / void type `()`.
552    Void,
553    Bool,
554    Char,
555    String,
556    U8,
557    U16,
558    U32,
559    U64,
560    U128,
561    I8,
562    I16,
563    I32,
564    I64,
565    I128,
566    /// Identifier of an actor / service instance.
567    ActorId,
568    /// Identifier of deployed code.
569    CodeId,
570    /// Identifier of a message.
571    MessageId,
572    /// 160-bit hash / address type.
573    H160,
574    /// 256-bit hash (32-byte array) type.
575    H256,
576    /// 256-bit unsigned integer type.
577    U256,
578}
579
580impl PrimitiveType {
581    /// Returns the canonical textual representation used when rendering IDL.
582    pub const fn as_str(&self) -> &'static str {
583        use PrimitiveType::*;
584        match self {
585            Void => "()",
586            Bool => "bool",
587            Char => "char",
588            String => "String",
589            U8 => "u8",
590            U16 => "u16",
591            U32 => "u32",
592            U64 => "u64",
593            U128 => "u128",
594            I8 => "i8",
595            I16 => "i16",
596            I32 => "i32",
597            I64 => "i64",
598            I128 => "i128",
599            ActorId => "ActorId",
600            CodeId => "CodeId",
601            MessageId => "MessageId",
602            H160 => "H160",
603            H256 => "H256",
604            U256 => "U256",
605        }
606    }
607}
608
609impl Display for PrimitiveType {
610    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
611        f.write_str(self.as_str())
612    }
613}
614
615impl core::str::FromStr for PrimitiveType {
616    type Err = String;
617
618    /// Parses a primitive type from its textual representation used in IDL.
619    ///
620    /// Accepts several common aliases and case variations (e.g. `string` / `String`,
621    /// `actor_id` / `ActorId`) for convenience.
622    fn from_str(s: &str) -> Result<Self, Self::Err> {
623        use PrimitiveType::*;
624        match s {
625            "()" => Ok(Void),
626            "bool" => Ok(Bool),
627            "char" => Ok(Char),
628            "String" | "string" => Ok(String),
629            "u8" => Ok(U8),
630            "u16" => Ok(U16),
631            "u32" => Ok(U32),
632            "u64" => Ok(U64),
633            "u128" => Ok(U128),
634            "i8" => Ok(I8),
635            "i16" => Ok(I16),
636            "i32" => Ok(I32),
637            "i64" => Ok(I64),
638            "i128" => Ok(I128),
639
640            "ActorId" | "actor" | "actor_id" => Ok(ActorId),
641            "CodeId" | "code" | "code_id" => Ok(CodeId),
642            "MessageId" | "messageid" | "message_id" => Ok(MessageId),
643
644            "H256" | "h256" => Ok(H256),
645            "U256" | "u256" => Ok(U256),
646            "H160" | "h160" => Ok(H160),
647
648            other => Err(format!("Unknown primitive type: {other}")),
649        }
650    }
651}
652
653/// Top-level named type definition inside `types { ... }` of a service or program.
654///
655/// `Type` describes either a struct or enum with an optional list of generic
656/// type parameters, along with documentation and annotations taken from IDL.
657#[derive(Debug, Clone, PartialEq)]
658#[cfg_attr(
659    feature = "templates",
660    derive(askama::Template),
661    template(path = "type.askama", escape = "none")
662)]
663#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
664pub struct Type {
665    pub name: String,
666    #[cfg_attr(
667        feature = "serde",
668        serde(default, skip_serializing_if = "Vec::is_empty")
669    )]
670    pub type_params: Vec<TypeParameter>,
671    #[cfg_attr(feature = "serde", serde(flatten))]
672    pub def: TypeDef,
673    #[cfg_attr(
674        feature = "serde",
675        serde(default, skip_serializing_if = "Vec::is_empty")
676    )]
677    pub docs: Vec<String>,
678    #[cfg_attr(
679        feature = "serde",
680        serde(default, skip_serializing_if = "Vec::is_empty")
681    )]
682    pub annotations: Vec<Annotation>,
683}
684
685impl Type {
686    #[cfg(feature = "serde")]
687    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
688        serde_json::to_string(self)
689    }
690
691    #[cfg(feature = "serde")]
692    pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
693        serde_json::to_string_pretty(self)
694    }
695}
696
697/// Generic type parameter in a type definition.
698///
699/// - `name` is the declared identifier of the parameter (e.g. `T`);
700/// - `ty` is an optional concrete type bound / substitution; `None` means that
701///   the parameter is left generic at this level.
702#[derive(Debug, PartialEq, Eq, Hash, Clone)]
703#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
704pub struct TypeParameter {
705    /// The name of the generic type parameter e.g. "T".
706    pub name: String,
707    /// The concrete type for the type parameter.
708    ///
709    /// `None` if the type parameter is skipped.
710    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
711    pub ty: Option<TypeDecl>,
712}
713
714impl Display for TypeParameter {
715    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
716        let TypeParameter { name, ty: _ } = self;
717        write!(f, "{name}")
718    }
719}
720
721/// Underlying definition of a named type: either a struct or an enum.
722///
723/// This mirrors the two composite categories in the IDL:
724/// - `Struct` - record / tuple / unit structs;
725/// - `Enum` - tagged unions with variants that may carry payloads.
726#[derive(Debug, Clone, PartialEq)]
727#[cfg_attr(
728    feature = "serde",
729    derive(Serialize, Deserialize),
730    serde(rename_all = "lowercase", tag = "kind")
731)]
732pub enum TypeDef {
733    Struct(StructDef),
734    Enum(EnumDef),
735    Alias(AliasDef),
736}
737
738/// Struct definition backing a named type or an enum variant payload.
739///
740/// A struct can represent:
741/// - unit form (`fields.is_empty()`),
742/// - classic form with named fields,
743/// - tuple-like form with unnamed fields.
744#[derive(Debug, Clone, PartialEq)]
745#[cfg_attr(
746    feature = "templates",
747    derive(askama::Template),
748    template(path = "struct_def.askama", escape = "none")
749)]
750#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
751pub struct StructDef {
752    #[cfg_attr(feature = "serde", serde(default))]
753    pub fields: Vec<StructField>,
754}
755
756impl StructDef {
757    /// Returns `true` if the struct has no fields (unit struct).
758    pub fn is_unit(&self) -> bool {
759        self.fields.is_empty()
760    }
761
762    /// Returns `true` if the struct is inline and purely positional:
763    /// all fields are unnamed and have no docs or annotations.
764    pub fn is_inline(&self) -> bool {
765        self.fields
766            .iter()
767            .all(|f| f.name.is_none() && f.docs.is_empty() && f.annotations.is_empty())
768    }
769
770    /// Returns `true` if the struct is tuple-like (all fields are unnamed).
771    pub fn is_tuple(&self) -> bool {
772        self.fields.iter().all(|f| f.name.is_none())
773    }
774
775    #[cfg(feature = "serde")]
776    pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
777        serde_json::to_string(self)
778    }
779
780    #[cfg(feature = "serde")]
781    pub fn to_json_string_pretty(&self) -> Result<String, serde_json::Error> {
782        serde_json::to_string_pretty(self)
783    }
784}
785
786/// Field of a struct or of an enum variant payload.
787///
788/// `name` is `None` for tuple-like structs / variants; otherwise it stores the
789/// field identifier from IDL. Each field keeps its own documentation and annotations.
790#[derive(Debug, Clone, PartialEq)]
791#[cfg_attr(
792    feature = "templates",
793    derive(askama::Template),
794    template(path = "field.askama", escape = "none")
795)]
796#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
797pub struct StructField {
798    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
799    pub name: Option<String>,
800    #[cfg_attr(feature = "serde", serde(rename = "type"))]
801    pub type_decl: TypeDecl,
802    #[cfg_attr(
803        feature = "serde",
804        serde(default, skip_serializing_if = "Vec::is_empty")
805    )]
806    pub docs: Vec<String>,
807    #[cfg_attr(
808        feature = "serde",
809        serde(default, skip_serializing_if = "Vec::is_empty")
810    )]
811    pub annotations: Vec<Annotation>,
812}
813
814/// Enum definition backing a named enum type.
815///
816/// Stores the ordered list of `EnumVariant` items that form a tagged union.
817/// Each variant may be unit-like, classic (named fields) or tuple-like.
818#[derive(Debug, Clone, PartialEq)]
819#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
820pub struct EnumDef {
821    #[cfg_attr(feature = "serde", serde(default))]
822    pub variants: Vec<EnumVariant>,
823}
824
825/// Single variant of an enum or service event.
826///
827/// - `name` is the variant identifier,
828/// - `def` is a `StructDef` describing the payload shape (unit / classic / tuple),
829/// - `entry_id` is the on-chain entry identifier; meaningful for service events,
830///   computed by [`ServiceUnit::normalize`] from `@entry_id` annotation or declaration order,
831/// - `docs` and `annotations` are attached to the variant in IDL.
832#[derive(Debug, Clone, PartialEq)]
833#[cfg_attr(
834    feature = "templates",
835    derive(askama::Template),
836    template(path = "variant.askama", escape = "none")
837)]
838#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
839pub struct EnumVariant {
840    pub name: String,
841    #[cfg_attr(feature = "serde", serde(flatten))]
842    pub def: StructDef,
843    #[cfg_attr(feature = "serde", serde(default))]
844    pub entry_id: u16,
845    #[cfg_attr(
846        feature = "serde",
847        serde(default, skip_serializing_if = "Vec::is_empty")
848    )]
849    pub docs: Vec<String>,
850    #[cfg_attr(
851        feature = "serde",
852        serde(default, skip_serializing_if = "Vec::is_empty")
853    )]
854    pub annotations: Vec<Annotation>,
855}
856
857/// Alias definition backing a named alias type.
858#[derive(Debug, Clone, PartialEq)]
859#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
860pub struct AliasDef {
861    pub target: TypeDecl,
862}
863
864#[cfg(feature = "serde")]
865mod serde_str {
866    use super::*;
867    use core::str::FromStr;
868    use serde::{Deserializer, Serializer};
869
870    pub(super) fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
871    where
872        T: Display,
873        S: Serializer,
874    {
875        serializer.collect_str(value)
876    }
877
878    pub(super) fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
879    where
880        T: FromStr,
881        <T as FromStr>::Err: Display,
882        D: Deserializer<'de>,
883    {
884        let s = String::deserialize(deserializer)?;
885        T::from_str(s.as_str()).map_err(serde::de::Error::custom)
886    }
887}
888
889#[cfg(feature = "serde")]
890mod serde_opt_str {
891    use super::*;
892    use core::str::FromStr;
893    use serde::{Deserializer, Serializer};
894
895    pub(super) fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
896    where
897        T: Display,
898        S: Serializer,
899    {
900        match value {
901            Some(value) => serializer.collect_str(value),
902            None => serializer.serialize_none(),
903        }
904    }
905
906    pub(super) fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
907    where
908        T: FromStr,
909        <T as FromStr>::Err: Display,
910        D: Deserializer<'de>,
911    {
912        let opt = Option::<String>::deserialize(deserializer)?;
913        opt.map(|s| T::from_str(s.as_str()).map_err(serde::de::Error::custom))
914            .transpose()
915    }
916}