Skip to main content

reflectapi_schema/
normalize.rs

1/// Normalization pipeline for transforming raw schemas into semantic IRs
2///
3/// This module provides the core normalization passes that transform
4/// the raw reflectapi_schema types into validated, immutable semantic
5/// representations with deterministic ordering and resolved dependencies.
6use crate::symbol::{STDLIB_TYPES, STDLIB_TYPE_PREFIXES};
7use crate::{
8    Enum, Field, FieldStyle, Fields, Function, Primitive, ResolvedTypeReference, Schema,
9    SemanticEnum, SemanticField, SemanticFunction, SemanticOutputType, SemanticPrimitive,
10    SemanticSchema, SemanticStruct, SemanticType, SemanticTypeParameter, SemanticVariant, Struct,
11    SymbolId, SymbolInfo, SymbolKind, SymbolTable, Type, TypeReference, Variant,
12};
13use std::collections::{BTreeMap, BTreeSet, HashMap};
14
15/// Trait for individual normalization stages in the pipeline
16pub trait NormalizationStage {
17    fn name(&self) -> &'static str;
18    fn transform(&self, schema: &mut Schema) -> Result<(), Vec<NormalizationError>>;
19}
20
21/// Normalization pipeline that applies multiple stages in sequence
22#[derive(Default)]
23pub struct NormalizationPipeline {
24    stages: Vec<Box<dyn NormalizationStage>>,
25}
26
27impl NormalizationPipeline {
28    pub fn new() -> Self {
29        Self { stages: Vec::new() }
30    }
31
32    pub fn add_stage<S: NormalizationStage + 'static>(mut self, stage: S) -> Self {
33        self.stages.push(Box::new(stage));
34        self
35    }
36
37    pub fn run(&self, schema: &mut Schema) -> Result<(), Vec<NormalizationError>> {
38        for stage in &self.stages {
39            stage.transform(schema)?;
40        }
41        Ok(())
42    }
43
44    /// Create the standard normalization pipeline.
45    ///
46    /// Delegates to `PipelineBuilder` with all default settings.
47    pub fn standard() -> Self {
48        PipelineBuilder::new().build()
49    }
50
51    /// Create a codegen-oriented pipeline that only runs CircularDependencyResolution.
52    ///
53    /// This is designed for use when the caller has already run
54    /// `schema.consolidate_types()` and does not want NamingResolution
55    /// (which would rename types and create a name-domain mismatch
56    /// between the SemanticSchema and the raw Schema used for rendering).
57    ///
58    /// Delegates to `PipelineBuilder` with consolidation and naming skipped.
59    pub fn for_codegen() -> Self {
60        PipelineBuilder::new()
61            .consolidation(Consolidation::Skip)
62            .naming(Naming::Skip)
63            .build()
64    }
65}
66
67// ---------------------------------------------------------------------------
68// PipelineBuilder: configurable pipeline construction
69// ---------------------------------------------------------------------------
70
71/// Controls whether and how input/output types are merged.
72#[derive(Debug, Clone, Default)]
73pub enum Consolidation {
74    /// Run the standard `TypeConsolidationStage`.
75    #[default]
76    Standard,
77    /// Skip type consolidation entirely.
78    Skip,
79}
80
81/// Controls how type names are resolved.
82#[derive(Default)]
83pub enum Naming {
84    /// Run the standard `NamingResolutionStage`.
85    #[default]
86    Standard,
87    /// Skip naming resolution entirely.
88    Skip,
89    /// Use a custom naming stage.
90    Custom(Box<dyn NormalizationStage>),
91}
92
93impl std::fmt::Debug for Naming {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            Naming::Standard => write!(f, "Naming::Standard"),
97            Naming::Skip => write!(f, "Naming::Skip"),
98            Naming::Custom(_) => write!(f, "Naming::Custom(...)"),
99        }
100    }
101}
102
103/// Builder for configuring a normalization pipeline.
104///
105/// Provides fine-grained control over which normalization stages are included
106/// and in what order. The default configuration matches `NormalizationPipeline::standard()`.
107///
108/// # Examples
109///
110/// ```rust,ignore
111/// // Standard pipeline (equivalent to NormalizationPipeline::standard())
112/// let pipeline = PipelineBuilder::new().build();
113///
114/// // Codegen pipeline (equivalent to NormalizationPipeline::for_codegen())
115/// let pipeline = PipelineBuilder::new()
116///     .consolidation(Consolidation::Skip)
117///     .naming(Naming::Skip)
118///     .build();
119///
120/// // Custom pipeline with extra stages
121/// let pipeline = PipelineBuilder::new()
122///     .circular_dependency_strategy(ResolutionStrategy::Boxing)
123///     .add_stage(MyCustomStage)
124///     .build();
125/// ```
126pub struct PipelineBuilder {
127    consolidation: Consolidation,
128    naming: Naming,
129    circular_dependency_strategy: ResolutionStrategy,
130    extra_stages: Vec<Box<dyn NormalizationStage>>,
131}
132
133impl Default for PipelineBuilder {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl PipelineBuilder {
140    /// Create a new builder with default settings (all stages enabled).
141    pub fn new() -> Self {
142        Self {
143            consolidation: Consolidation::default(),
144            naming: Naming::default(),
145            circular_dependency_strategy: ResolutionStrategy::default(),
146            extra_stages: Vec::new(),
147        }
148    }
149
150    /// Set the consolidation strategy.
151    pub fn consolidation(mut self, consolidation: Consolidation) -> Self {
152        self.consolidation = consolidation;
153        self
154    }
155
156    /// Set the naming resolution strategy.
157    pub fn naming(mut self, naming: Naming) -> Self {
158        self.naming = naming;
159        self
160    }
161
162    /// Set the circular dependency resolution strategy.
163    pub fn circular_dependency_strategy(mut self, strategy: ResolutionStrategy) -> Self {
164        self.circular_dependency_strategy = strategy;
165        self
166    }
167
168    /// Append a custom stage that will run after the built-in stages.
169    pub fn add_stage<S: NormalizationStage + 'static>(mut self, stage: S) -> Self {
170        self.extra_stages.push(Box::new(stage));
171        self
172    }
173
174    /// Build the configured `NormalizationPipeline`.
175    ///
176    /// Stages are added in order:
177    /// 1. Type consolidation (if not skipped)
178    /// 2. Naming resolution (if not skipped, or custom stage)
179    /// 3. Circular dependency resolution (always included)
180    /// 4. Any extra stages added via `add_stage()`
181    pub fn build(self) -> NormalizationPipeline {
182        let mut pipeline = NormalizationPipeline::new();
183
184        match self.consolidation {
185            Consolidation::Standard => {
186                pipeline = pipeline.add_stage(TypeConsolidationStage);
187            }
188            Consolidation::Skip => {}
189        }
190
191        match self.naming {
192            Naming::Standard => {
193                pipeline = pipeline.add_stage(NamingResolutionStage);
194            }
195            Naming::Skip => {}
196            Naming::Custom(stage) => {
197                pipeline.stages.push(stage);
198            }
199        }
200
201        pipeline = pipeline.add_stage(CircularDependencyResolutionStage::with_strategy(
202            self.circular_dependency_strategy,
203        ));
204
205        for stage in self.extra_stages {
206            pipeline.stages.push(stage);
207        }
208
209        pipeline
210    }
211}
212
213// ---------------------------------------------------------------------------
214// Stage 1: Type Consolidation
215// ---------------------------------------------------------------------------
216
217/// Merges input_types and output_types into a single unified types collection.
218/// Handles naming conflicts by renaming types with prefixes.
219pub struct TypeConsolidationStage;
220
221impl NormalizationStage for TypeConsolidationStage {
222    fn name(&self) -> &'static str {
223        "TypeConsolidation"
224    }
225
226    fn transform(&self, schema: &mut Schema) -> Result<(), Vec<NormalizationError>> {
227        use crate::Typespace;
228
229        let mut consolidated = Typespace::new();
230        let mut name_conflicts = HashMap::new();
231        // Tracks old_name -> new_name for type reference rewriting
232        let mut rename_map: HashMap<String, String> = HashMap::new();
233
234        let mut input_type_names = HashMap::new();
235        let mut output_type_names = HashMap::new();
236
237        for ty in schema.input_types.types() {
238            let simple_name = extract_simple_name(ty.name());
239            input_type_names.insert(simple_name.clone(), ty.clone());
240
241            if output_type_names.contains_key(&simple_name) {
242                name_conflicts.insert(simple_name, true);
243            }
244        }
245
246        for ty in schema.output_types.types() {
247            let simple_name = extract_simple_name(ty.name());
248            output_type_names.insert(simple_name.clone(), ty.clone());
249
250            if input_type_names.contains_key(&simple_name) {
251                name_conflicts.insert(simple_name, true);
252            }
253        }
254
255        for ty in schema.input_types.types() {
256            let simple_name = extract_simple_name(ty.name());
257            let mut new_type = ty.clone();
258
259            if name_conflicts.contains_key(&simple_name) {
260                let old_name = ty.name().to_string();
261                let new_name = format!("input.{}", ty.name().replace("::", "."));
262                rename_type(&mut new_type, &new_name);
263                rename_map.insert(old_name, new_name);
264            }
265
266            consolidated.insert_type(new_type);
267        }
268
269        for ty in schema.output_types.types() {
270            let simple_name = extract_simple_name(ty.name());
271            let mut new_type = ty.clone();
272
273            if name_conflicts.contains_key(&simple_name) {
274                let old_name = ty.name().to_string();
275                let new_name = format!("output.{}", ty.name().replace("::", "."));
276                rename_type(&mut new_type, &new_name);
277                rename_map.insert(old_name, new_name);
278                consolidated.insert_type(new_type);
279            } else if !input_type_names.contains_key(&simple_name) {
280                consolidated.insert_type(new_type);
281            }
282        }
283
284        schema.input_types = consolidated;
285        schema.output_types = Typespace::new();
286
287        // Rewrite type references that still point to old names
288        if !rename_map.is_empty() {
289            for function in &mut schema.functions {
290                update_type_reference_in_option(&mut function.input_type, &rename_map);
291                update_type_reference_in_option(&mut function.input_headers, &rename_map);
292                update_type_references_in_output_type(&mut function.output_type, &rename_map);
293                update_type_reference_in_option(&mut function.error_type, &rename_map);
294            }
295
296            let types_to_update: Vec<_> = schema.input_types.types().cloned().collect();
297            schema.input_types = Typespace::new();
298            for mut ty in types_to_update {
299                update_type_references_in_type(&mut ty, &rename_map);
300                schema.input_types.insert_type(ty);
301            }
302        }
303
304        Ok(())
305    }
306}
307
308fn extract_simple_name(qualified_name: &str) -> String {
309    qualified_name
310        .split("::")
311        .last()
312        .unwrap_or(qualified_name)
313        .to_string()
314}
315
316fn rename_type(ty: &mut Type, new_name: &str) {
317    let new_path: Vec<String> = new_name.split("::").map(|s| s.to_string()).collect();
318    match ty {
319        Type::Struct(s) => {
320            s.name = new_name.to_string();
321            s.id.path = new_path;
322        }
323        Type::Enum(e) => {
324            e.name = new_name.to_string();
325            e.id.path = new_path;
326        }
327        Type::Primitive(p) => {
328            p.name = new_name.to_string();
329            p.id.path = new_path;
330        }
331    }
332}
333
334// ---------------------------------------------------------------------------
335// Stage 2: Naming Resolution
336// ---------------------------------------------------------------------------
337
338/// Sanitizes type names by stripping module paths and handling naming conflicts.
339pub struct NamingResolutionStage;
340
341impl NormalizationStage for NamingResolutionStage {
342    fn name(&self) -> &'static str {
343        "NamingResolution"
344    }
345
346    fn transform(&self, schema: &mut Schema) -> Result<(), Vec<NormalizationError>> {
347        let mut name_usage: HashMap<String, Vec<String>> = HashMap::new();
348        let mut name_conflicts = HashMap::new();
349
350        for ty in schema.input_types.types() {
351            let qualified_name = ty.name().to_string();
352            let simple_name = extract_simple_name(&qualified_name);
353
354            let entries = name_usage.entry(simple_name.clone()).or_default();
355            if !entries.contains(&qualified_name) {
356                if !entries.is_empty() {
357                    name_conflicts.insert(simple_name.clone(), true);
358                }
359                entries.push(qualified_name);
360            }
361        }
362
363        let types_to_update: Vec<_> = schema.input_types.types().cloned().collect();
364        schema.input_types = crate::Typespace::new();
365
366        for mut ty in types_to_update {
367            let qualified_name = ty.name().to_string();
368            let simple_name = extract_simple_name(&qualified_name);
369
370            let resolved_name = if name_conflicts.contains_key(&simple_name) {
371                generate_unique_name(&qualified_name)
372            } else {
373                simple_name
374            };
375
376            rename_type(&mut ty, &resolved_name);
377            schema.input_types.insert_type(ty);
378        }
379
380        update_type_references_in_schema(schema, &name_usage, &name_conflicts);
381
382        Ok(())
383    }
384}
385
386fn generate_unique_name(qualified_name: &str) -> String {
387    let parts: Vec<&str> = qualified_name.split("::").collect();
388    if parts.len() < 2 {
389        return qualified_name.to_string();
390    }
391
392    let type_name = parts.last().unwrap();
393    let module_parts: Vec<&str> = parts[..parts.len() - 1].to_vec();
394
395    let non_excluded: Vec<&str> = module_parts
396        .iter()
397        .filter(|&&part| part != "model" && part != "proto" && !part.is_empty())
398        .copied()
399        .collect();
400
401    let prefix = if non_excluded.is_empty() {
402        module_parts.join("_")
403    } else {
404        non_excluded
405            .iter()
406            .map(|s| capitalize_first_letter(s))
407            .collect::<Vec<_>>()
408            .join("")
409    };
410    format!("{prefix}{type_name}")
411}
412
413fn capitalize_first_letter(s: &str) -> String {
414    let mut chars = s.chars();
415    match chars.next() {
416        None => String::new(),
417        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
418    }
419}
420
421fn update_type_references_in_schema(
422    schema: &mut Schema,
423    name_usage: &HashMap<String, Vec<String>>,
424    name_conflicts: &HashMap<String, bool>,
425) {
426    let mut name_mapping = HashMap::new();
427
428    for (simple_name, qualified_names) in name_usage {
429        if name_conflicts.contains_key(simple_name) {
430            for qualified_name in qualified_names {
431                let resolved_name = generate_unique_name(qualified_name);
432                name_mapping.insert(qualified_name.clone(), resolved_name);
433            }
434        } else {
435            for qualified_name in qualified_names {
436                name_mapping.insert(qualified_name.clone(), simple_name.clone());
437            }
438        }
439    }
440
441    for function in &mut schema.functions {
442        update_type_reference_in_option(&mut function.input_type, &name_mapping);
443        update_type_reference_in_option(&mut function.input_headers, &name_mapping);
444        update_type_references_in_output_type(&mut function.output_type, &name_mapping);
445        update_type_reference_in_option(&mut function.error_type, &name_mapping);
446    }
447
448    let types_to_update: Vec<_> = schema.input_types.types().cloned().collect();
449    schema.input_types = crate::Typespace::new();
450
451    for mut ty in types_to_update {
452        update_type_references_in_type(&mut ty, &name_mapping);
453        schema.input_types.insert_type(ty);
454    }
455}
456
457fn update_type_reference(
458    type_ref: &mut crate::TypeReference,
459    name_mapping: &HashMap<String, String>,
460) {
461    if let Some(new_name) = name_mapping.get(&type_ref.name) {
462        type_ref.name.clone_from(new_name);
463    }
464
465    for arg in &mut type_ref.arguments {
466        update_type_reference(arg, name_mapping);
467    }
468}
469
470fn update_type_reference_in_option(
471    type_ref_opt: &mut Option<crate::TypeReference>,
472    name_mapping: &HashMap<String, String>,
473) {
474    if let Some(type_ref) = type_ref_opt {
475        update_type_reference(type_ref, name_mapping);
476    }
477}
478
479fn update_type_references_in_output_type(
480    output_type: &mut crate::OutputType,
481    name_mapping: &HashMap<String, String>,
482) {
483    match output_type {
484        crate::OutputType::Complete { output_type } => {
485            update_type_reference_in_option(output_type, name_mapping);
486        }
487        crate::OutputType::Stream { item_type } => {
488            update_type_reference(item_type, name_mapping);
489        }
490    }
491}
492
493fn update_type_references_in_type(ty: &mut crate::Type, name_mapping: &HashMap<String, String>) {
494    match ty {
495        crate::Type::Struct(s) => match &mut s.fields {
496            crate::Fields::Named(fields) | crate::Fields::Unnamed(fields) => {
497                for field in fields {
498                    update_type_reference(&mut field.type_ref, name_mapping);
499                }
500            }
501            crate::Fields::None => {}
502        },
503        crate::Type::Enum(e) => {
504            for variant in &mut e.variants {
505                match &mut variant.fields {
506                    crate::Fields::Named(fields) | crate::Fields::Unnamed(fields) => {
507                        for field in fields {
508                            update_type_reference(&mut field.type_ref, name_mapping);
509                        }
510                    }
511                    crate::Fields::None => {}
512                }
513            }
514        }
515        crate::Type::Primitive(p) => {
516            if let Some(fallback) = &mut p.fallback {
517                update_type_reference(fallback, name_mapping);
518            }
519        }
520    }
521}
522
523// ---------------------------------------------------------------------------
524// Stage 3: Circular Dependency Resolution
525// ---------------------------------------------------------------------------
526
527/// Detects and resolves circular dependencies using Tarjan's SCC algorithm
528/// and configurable resolution strategies.
529pub struct CircularDependencyResolutionStage {
530    strategy: ResolutionStrategy,
531}
532
533#[derive(Debug, Clone, Default)]
534pub enum ResolutionStrategy {
535    /// Try boxing first, then forward declarations
536    #[default]
537    Intelligent,
538    /// Always use Box<T> for self-references
539    Boxing,
540    /// Always use forward declarations
541    ForwardDeclarations,
542    /// Make circular references optional
543    OptionalBreaking,
544    /// Use reference counting for complex cycles
545    ReferenceCounted,
546}
547
548impl CircularDependencyResolutionStage {
549    pub fn new() -> Self {
550        Self {
551            strategy: ResolutionStrategy::default(),
552        }
553    }
554
555    pub fn with_strategy(strategy: ResolutionStrategy) -> Self {
556        Self { strategy }
557    }
558}
559
560impl Default for CircularDependencyResolutionStage {
561    fn default() -> Self {
562        Self::new()
563    }
564}
565
566impl NormalizationStage for CircularDependencyResolutionStage {
567    fn name(&self) -> &'static str {
568        "CircularDependencyResolution"
569    }
570
571    fn transform(&self, schema: &mut Schema) -> Result<(), Vec<NormalizationError>> {
572        let cycles = self.detect_circular_dependencies(schema)?;
573
574        if cycles.is_empty() {
575            return Ok(());
576        }
577
578        for cycle in cycles {
579            self.resolve_cycle(schema, &cycle)?;
580        }
581
582        Ok(())
583    }
584}
585
586impl CircularDependencyResolutionStage {
587    fn detect_circular_dependencies(
588        &self,
589        schema: &Schema,
590    ) -> Result<Vec<Vec<String>>, Vec<NormalizationError>> {
591        let mut dependencies: HashMap<String, BTreeSet<String>> = HashMap::new();
592
593        for ty in schema
594            .input_types
595            .types()
596            .chain(schema.output_types.types())
597        {
598            let type_name = ty.name().to_string();
599            let mut deps = BTreeSet::new();
600            self.collect_type_dependencies(ty, &mut deps);
601            dependencies.insert(type_name, deps);
602        }
603
604        let scc_cycles = self.find_strongly_connected_components(&dependencies);
605
606        let mut cycles = Vec::new();
607        for component in scc_cycles {
608            if component.len() > 1
609                || (component.len() == 1
610                    && dependencies
611                        .get(&component[0])
612                        .is_some_and(|deps| deps.contains(&component[0])))
613            {
614                cycles.push(component);
615            }
616        }
617
618        Ok(cycles)
619    }
620
621    fn collect_type_dependencies(&self, ty: &Type, deps: &mut BTreeSet<String>) {
622        match ty {
623            Type::Struct(s) => {
624                for field in s.fields() {
625                    self.collect_type_ref_dependencies(&field.type_ref, deps);
626                }
627            }
628            Type::Enum(e) => {
629                for variant in e.variants() {
630                    for field in variant.fields() {
631                        self.collect_type_ref_dependencies(&field.type_ref, deps);
632                    }
633                }
634            }
635            Type::Primitive(p) => {
636                if let Some(fallback) = &p.fallback {
637                    self.collect_type_ref_dependencies(fallback, deps);
638                }
639            }
640        }
641    }
642
643    fn collect_type_ref_dependencies(&self, type_ref: &TypeReference, deps: &mut BTreeSet<String>) {
644        if !self.is_stdlib_type(&type_ref.name) && !self.is_generic_parameter(&type_ref.name) {
645            deps.insert(type_ref.name.clone());
646        }
647
648        for arg in &type_ref.arguments {
649            self.collect_type_ref_dependencies(arg, deps);
650        }
651    }
652
653    fn is_stdlib_type(&self, name: &str) -> bool {
654        // Check exact matches from the canonical list
655        if STDLIB_TYPES.iter().any(|&(n, _)| n == name) {
656            return true;
657        }
658        // Fall back to prefix matching for types not explicitly listed
659        STDLIB_TYPE_PREFIXES
660            .iter()
661            .any(|prefix| name.starts_with(prefix))
662    }
663
664    fn is_generic_parameter(&self, name: &str) -> bool {
665        name.len() <= 2 && name.chars().all(|c| c.is_ascii_uppercase())
666    }
667
668    fn find_strongly_connected_components(
669        &self,
670        dependencies: &HashMap<String, BTreeSet<String>>,
671    ) -> Vec<Vec<String>> {
672        let mut index = 0;
673        let mut stack = Vec::new();
674        let mut indices: HashMap<String, usize> = HashMap::new();
675        let mut lowlinks: HashMap<String, usize> = HashMap::new();
676        let mut on_stack: HashMap<String, bool> = HashMap::new();
677        let mut components = Vec::new();
678
679        for node in dependencies.keys() {
680            if !indices.contains_key(node) {
681                self.strongconnect(
682                    node,
683                    dependencies,
684                    &mut index,
685                    &mut stack,
686                    &mut indices,
687                    &mut lowlinks,
688                    &mut on_stack,
689                    &mut components,
690                );
691            }
692        }
693
694        components
695    }
696
697    #[allow(clippy::too_many_arguments, clippy::only_used_in_recursion)]
698    fn strongconnect(
699        &self,
700        node: &str,
701        dependencies: &HashMap<String, BTreeSet<String>>,
702        index: &mut usize,
703        stack: &mut Vec<String>,
704        indices: &mut HashMap<String, usize>,
705        lowlinks: &mut HashMap<String, usize>,
706        on_stack: &mut HashMap<String, bool>,
707        components: &mut Vec<Vec<String>>,
708    ) {
709        indices.insert(node.to_string(), *index);
710        lowlinks.insert(node.to_string(), *index);
711        *index += 1;
712        stack.push(node.to_string());
713        on_stack.insert(node.to_string(), true);
714
715        if let Some(deps) = dependencies.get(node) {
716            for neighbor in deps {
717                if !indices.contains_key(neighbor) {
718                    self.strongconnect(
719                        neighbor,
720                        dependencies,
721                        index,
722                        stack,
723                        indices,
724                        lowlinks,
725                        on_stack,
726                        components,
727                    );
728                    lowlinks.insert(node.to_string(), lowlinks[node].min(lowlinks[neighbor]));
729                } else if *on_stack.get(neighbor).unwrap_or(&false) {
730                    lowlinks.insert(node.to_string(), lowlinks[node].min(indices[neighbor]));
731                }
732            }
733        }
734
735        if lowlinks[node] == indices[node] {
736            let mut component = Vec::new();
737            loop {
738                let w = stack.pop().unwrap();
739                on_stack.insert(w.clone(), false);
740                component.push(w.clone());
741                if w == node {
742                    break;
743                }
744            }
745            if !component.is_empty() {
746                components.push(component);
747            }
748        }
749    }
750
751    fn resolve_cycle(
752        &self,
753        schema: &mut Schema,
754        cycle: &[String],
755    ) -> Result<(), Vec<NormalizationError>> {
756        match self.strategy {
757            ResolutionStrategy::Intelligent => {
758                if cycle.len() == 1 {
759                    self.apply_boxing_strategy(schema, cycle)
760                } else {
761                    self.apply_forward_declaration_strategy(schema, cycle)
762                }
763            }
764            ResolutionStrategy::Boxing => self.apply_boxing_strategy(schema, cycle),
765            ResolutionStrategy::ForwardDeclarations => {
766                self.apply_forward_declaration_strategy(schema, cycle)
767            }
768            ResolutionStrategy::OptionalBreaking => {
769                self.apply_optional_breaking_strategy(schema, cycle)
770            }
771            ResolutionStrategy::ReferenceCounted => {
772                self.apply_reference_counting_strategy(schema, cycle)
773            }
774        }
775    }
776
777    /// No-op: Rust schemas already encode `Box<T>` in the type references, so
778    /// self-referential types (cycle length 1) and multi-type cycles (A → B → A)
779    /// are already representable.  The cycle detection performed by the
780    /// `CircularDependencyResolutionStage` is still valuable — downstream codegen
781    /// backends (e.g. Python, TypeScript) can query the detected cycles to emit
782    /// forward-reference annotations or similar language-specific constructs.
783    fn apply_boxing_strategy(
784        &self,
785        _schema: &mut Schema,
786        _cycle: &[String],
787    ) -> Result<(), Vec<NormalizationError>> {
788        Ok(())
789    }
790
791    fn apply_forward_declaration_strategy(
792        &self,
793        _schema: &mut Schema,
794        _cycle: &[String],
795    ) -> Result<(), Vec<NormalizationError>> {
796        // TODO: Implement forward declarations by creating type aliases
797        Ok(())
798    }
799
800    fn apply_optional_breaking_strategy(
801        &self,
802        _schema: &mut Schema,
803        _cycle: &[String],
804    ) -> Result<(), Vec<NormalizationError>> {
805        // TODO: Make certain fields optional to break cycles
806        Ok(())
807    }
808
809    fn apply_reference_counting_strategy(
810        &self,
811        _schema: &mut Schema,
812        _cycle: &[String],
813    ) -> Result<(), Vec<NormalizationError>> {
814        // TODO: Wrap cycle references in Rc<RefCell<T>>
815        Ok(())
816    }
817}
818
819// ---------------------------------------------------------------------------
820// Error types
821// ---------------------------------------------------------------------------
822
823#[derive(Debug, Clone, PartialEq, Eq)]
824pub enum NormalizationError {
825    UnresolvedReference {
826        name: String,
827        referrer: SymbolId,
828    },
829    CircularDependency {
830        cycle: Vec<SymbolId>,
831    },
832    ConflictingDefinition {
833        symbol: SymbolId,
834        existing: String,
835        new: String,
836    },
837    InvalidGenericParameter {
838        type_name: String,
839        parameter: String,
840        reason: String,
841    },
842    ValidationError {
843        symbol: SymbolId,
844        message: String,
845    },
846}
847
848impl std::fmt::Display for NormalizationError {
849    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
850        match self {
851            NormalizationError::UnresolvedReference { name, referrer } => {
852                write!(
853                    f,
854                    "Unresolved type reference '{name}' in symbol {referrer:?}"
855                )
856            }
857            NormalizationError::CircularDependency { cycle } => {
858                write!(f, "Circular dependency detected: {cycle:?}")
859            }
860            NormalizationError::ConflictingDefinition {
861                symbol,
862                existing,
863                new,
864            } => {
865                write!(
866                    f,
867                    "Conflicting definition for symbol {symbol:?}: existing '{existing}', new '{new}'"
868                )
869            }
870            NormalizationError::InvalidGenericParameter {
871                type_name,
872                parameter,
873                reason,
874            } => {
875                write!(
876                    f,
877                    "Invalid generic parameter '{parameter}' in type '{type_name}': {reason}"
878                )
879            }
880            NormalizationError::ValidationError { symbol, message } => {
881                write!(f, "Validation error for symbol {symbol:?}: {message}")
882            }
883        }
884    }
885}
886
887impl std::error::Error for NormalizationError {}
888
889// ---------------------------------------------------------------------------
890// Normalizer: main pipeline converting Schema -> SemanticSchema
891// ---------------------------------------------------------------------------
892
893#[derive(Debug)]
894struct NormalizationContext {
895    symbol_table: SymbolTable,
896    raw_types: HashMap<SymbolId, Type>,
897    raw_functions: HashMap<SymbolId, Function>,
898    resolution_cache: HashMap<String, SymbolId>,
899    generic_scope: BTreeSet<String>,
900    errors: Vec<NormalizationError>,
901}
902
903impl Default for NormalizationContext {
904    fn default() -> Self {
905        Self::new()
906    }
907}
908
909impl NormalizationContext {
910    fn new() -> Self {
911        Self {
912            symbol_table: SymbolTable::new(),
913            raw_types: HashMap::new(),
914            raw_functions: HashMap::new(),
915            resolution_cache: HashMap::new(),
916            generic_scope: BTreeSet::new(),
917            errors: Vec::new(),
918        }
919    }
920
921    fn has_errors(&self) -> bool {
922        !self.errors.is_empty()
923    }
924
925    fn take_errors(&mut self) -> Vec<NormalizationError> {
926        std::mem::take(&mut self.errors)
927    }
928}
929
930/// Main normalizer that converts a raw Schema into a SemanticSchema
931pub struct Normalizer {
932    context: NormalizationContext,
933}
934
935impl Normalizer {
936    pub fn new() -> Self {
937        Self {
938            context: NormalizationContext::new(),
939        }
940    }
941
942    /// Normalize a raw schema into a semantic schema using the standard pipeline.
943    pub fn normalize(self, schema: &Schema) -> Result<SemanticSchema, Vec<NormalizationError>> {
944        self.normalize_with_pipeline(schema, NormalizationPipeline::standard())
945    }
946
947    /// Normalize a raw schema into a semantic schema using a custom pipeline.
948    ///
949    /// Use `PipelineBuilder` to configure which stages run, or the convenience
950    /// methods `NormalizationPipeline::standard()` / `NormalizationPipeline::for_codegen()`.
951    pub fn normalize_with_pipeline(
952        mut self,
953        schema: &Schema,
954        pipeline: NormalizationPipeline,
955    ) -> Result<SemanticSchema, Vec<NormalizationError>> {
956        // Clone so that pipeline stages can mutate without affecting the caller
957        let mut schema = schema.clone();
958
959        // Phase 0: Ensure all symbols have unique, stable IDs
960        crate::ids::ensure_symbol_ids(&mut schema);
961
962        // Capture original type names BEFORE the pipeline transforms them.
963        // NamingResolution (if present in the pipeline) will strip module
964        // paths, so we need to map short names back to qualified names.
965        let pre_norm_names: Vec<String> = schema
966            .input_types
967            .types()
968            .chain(schema.output_types.types())
969            .map(|t| t.name().to_string())
970            .collect();
971
972        // Run the caller-provided pipeline
973        pipeline.run(&mut schema)?;
974
975        // Build the original_names reverse mapping.
976        // When NamingResolution runs, it strips module paths (e.g.
977        // "my_module::MyType" -> "MyType"). We map the short name back
978        // to the pre-pipeline qualified name.
979        // When NamingResolution is NOT in the pipeline, names are unchanged
980        // and the mapping is identity — the unwrap_or fallback handles this.
981        let mut original_names: HashMap<String, String> = HashMap::new();
982        for pre_name in &pre_norm_names {
983            let short = pre_name.split("::").last().unwrap_or(pre_name);
984            original_names
985                .entry(short.to_string())
986                .or_insert_with(|| pre_name.clone());
987        }
988
989        // Phase 1: Symbol Discovery
990        self.discover_symbols(&schema)?;
991
992        // Phase 2: Type Resolution
993        self.resolve_types()?;
994
995        // Phase 3: Dependency Analysis
996        self.analyze_dependencies()?;
997
998        // Phase 4: Semantic Validation
999        self.validate_semantics()?;
1000
1001        // Phase 5: IR Construction
1002        self.build_semantic_ir(&schema, &original_names)
1003    }
1004
1005    fn discover_symbols(&mut self, schema: &Schema) -> Result<(), Vec<NormalizationError>> {
1006        let schema_info = SymbolInfo {
1007            id: schema.id.clone(),
1008            name: schema.name.clone(),
1009            path: schema.id.path.clone(),
1010            kind: SymbolKind::Struct,
1011            resolved: false,
1012            dependencies: BTreeSet::new(),
1013        };
1014        self.context.symbol_table.register(schema_info);
1015
1016        for function in &schema.functions {
1017            let function_info = SymbolInfo {
1018                id: function.id.clone(),
1019                name: function.name.clone(),
1020                path: function.id.path.clone(),
1021                kind: SymbolKind::Endpoint,
1022                resolved: false,
1023                dependencies: BTreeSet::new(),
1024            };
1025            self.context.symbol_table.register(function_info);
1026            self.context
1027                .raw_functions
1028                .insert(function.id.clone(), function.clone());
1029        }
1030
1031        self.discover_types_from_typespace(&schema.input_types);
1032        self.discover_types_from_typespace(&schema.output_types);
1033
1034        if self.context.has_errors() {
1035            return Err(self.context.take_errors());
1036        }
1037
1038        Ok(())
1039    }
1040
1041    fn discover_types_from_typespace(&mut self, typespace: &crate::Typespace) {
1042        for ty in typespace.types() {
1043            self.discover_type_symbols(ty);
1044        }
1045    }
1046
1047    fn discover_type_symbols(&mut self, ty: &Type) {
1048        let (id, name, kind) = match ty {
1049            Type::Primitive(p) => (p.id.clone(), p.name.clone(), SymbolKind::Primitive),
1050            Type::Struct(s) => (s.id.clone(), s.name.clone(), SymbolKind::Struct),
1051            Type::Enum(e) => (e.id.clone(), e.name.clone(), SymbolKind::Enum),
1052        };
1053
1054        let path = id.path.clone();
1055
1056        let symbol_info = SymbolInfo {
1057            id: id.clone(),
1058            name,
1059            path,
1060            kind,
1061            resolved: false,
1062            dependencies: BTreeSet::new(),
1063        };
1064
1065        self.context.symbol_table.register(symbol_info);
1066        self.context.raw_types.insert(id, ty.clone());
1067
1068        match ty {
1069            Type::Struct(s) => self.discover_struct_symbols(s),
1070            Type::Enum(e) => self.discover_enum_symbols(e),
1071            Type::Primitive(_) => {}
1072        }
1073    }
1074
1075    fn discover_struct_symbols(&mut self, strukt: &Struct) {
1076        for field in strukt.fields() {
1077            let field_info = SymbolInfo {
1078                id: field.id.clone(),
1079                name: field.name.clone(),
1080                path: field.id.path.clone(),
1081                kind: SymbolKind::Field,
1082                resolved: false,
1083                dependencies: BTreeSet::new(),
1084            };
1085            self.context.symbol_table.register(field_info);
1086        }
1087    }
1088
1089    fn discover_enum_symbols(&mut self, enm: &Enum) {
1090        for variant in enm.variants() {
1091            let variant_info = SymbolInfo {
1092                id: variant.id.clone(),
1093                name: variant.name.clone(),
1094                path: variant.id.path.clone(),
1095                kind: SymbolKind::Variant,
1096                resolved: false,
1097                dependencies: BTreeSet::new(),
1098            };
1099            self.context.symbol_table.register(variant_info);
1100
1101            for field in variant.fields() {
1102                let field_info = SymbolInfo {
1103                    id: field.id.clone(),
1104                    name: field.name.clone(),
1105                    path: field.id.path.clone(),
1106                    kind: SymbolKind::Field,
1107                    resolved: false,
1108                    dependencies: BTreeSet::new(),
1109                };
1110                self.context.symbol_table.register(field_info);
1111            }
1112        }
1113    }
1114
1115    fn resolve_types(&mut self) -> Result<(), Vec<NormalizationError>> {
1116        for symbol_info in self.context.symbol_table.symbols.values() {
1117            if !matches!(
1118                symbol_info.kind,
1119                SymbolKind::Struct
1120                    | SymbolKind::Enum
1121                    | SymbolKind::Primitive
1122                    | SymbolKind::TypeAlias
1123            ) {
1124                continue;
1125            }
1126            self.context
1127                .resolution_cache
1128                .insert(symbol_info.name.clone(), symbol_info.id.clone());
1129
1130            let qualified_name = symbol_info.id.qualified_name();
1131            if qualified_name != symbol_info.name {
1132                self.context
1133                    .resolution_cache
1134                    .insert(qualified_name, symbol_info.id.clone());
1135            }
1136        }
1137
1138        self.add_stdlib_types_to_cache();
1139
1140        for (function_id, function) in &self.context.raw_functions.clone() {
1141            self.resolve_function_references(function_id, function);
1142        }
1143
1144        for (type_id, ty) in &self.context.raw_types.clone() {
1145            self.resolve_type_references(type_id, ty);
1146        }
1147
1148        if self.context.has_errors() {
1149            return Err(self.context.take_errors());
1150        }
1151
1152        Ok(())
1153    }
1154
1155    fn resolve_function_references(&mut self, function_id: &SymbolId, function: &Function) {
1156        if let Some(input_type) = &function.input_type {
1157            self.resolve_single_reference(function_id, input_type);
1158        }
1159        if let Some(input_headers) = &function.input_headers {
1160            self.resolve_single_reference(function_id, input_headers);
1161        }
1162        match &function.output_type {
1163            crate::OutputType::Complete {
1164                output_type: Some(output_type),
1165            } => {
1166                self.resolve_single_reference(function_id, output_type);
1167            }
1168            crate::OutputType::Stream { item_type } => {
1169                self.resolve_single_reference(function_id, item_type);
1170            }
1171            crate::OutputType::Complete { output_type: None } => {}
1172        }
1173        if let Some(error_type) = &function.error_type {
1174            self.resolve_single_reference(function_id, error_type);
1175        }
1176    }
1177
1178    fn resolve_type_references(&mut self, type_id: &SymbolId, ty: &Type) {
1179        let generic_params: BTreeSet<String> = ty.parameters().map(|p| p.name.clone()).collect();
1180        self.context.generic_scope.extend(generic_params.clone());
1181
1182        match ty {
1183            Type::Struct(s) => {
1184                for field in s.fields() {
1185                    self.resolve_field_references(type_id, field);
1186                }
1187            }
1188            Type::Enum(e) => {
1189                for variant in e.variants() {
1190                    for field in variant.fields() {
1191                        self.resolve_field_references(type_id, field);
1192                    }
1193                }
1194            }
1195            Type::Primitive(p) => {
1196                if let Some(fallback) = &p.fallback {
1197                    self.resolve_single_reference(type_id, fallback);
1198                }
1199            }
1200        }
1201
1202        for param in generic_params {
1203            self.context.generic_scope.remove(&param);
1204        }
1205    }
1206
1207    fn resolve_field_references(&mut self, owner_id: &SymbolId, field: &Field) {
1208        self.resolve_single_reference(owner_id, &field.type_ref);
1209    }
1210
1211    fn add_stdlib_types_to_cache(&mut self) {
1212        for &(name, kind) in STDLIB_TYPES {
1213            let path = name.split("::").map(|s| s.to_string()).collect();
1214            let symbol_id = SymbolId::new(kind, path);
1215            self.context
1216                .resolution_cache
1217                .insert(name.to_string(), symbol_id);
1218        }
1219    }
1220
1221    fn resolve_single_reference(&mut self, referrer: &SymbolId, type_ref: &TypeReference) {
1222        if self.context.generic_scope.contains(&type_ref.name) {
1223            for arg in &type_ref.arguments {
1224                self.resolve_single_reference(referrer, arg);
1225            }
1226            return;
1227        }
1228
1229        if let Some(target_id) = self.resolve_global_type_reference(&type_ref.name) {
1230            self.context
1231                .symbol_table
1232                .add_dependency(referrer.clone(), target_id);
1233        }
1234
1235        for arg in &type_ref.arguments {
1236            self.resolve_single_reference(referrer, arg);
1237        }
1238    }
1239
1240    fn resolve_global_type_reference(&self, name: &str) -> Option<SymbolId> {
1241        self.context.resolution_cache.get(name).cloned()
1242    }
1243
1244    fn analyze_dependencies(&mut self) -> Result<(), Vec<NormalizationError>> {
1245        match self.context.symbol_table.topological_sort() {
1246            Ok(_) => Ok(()),
1247            Err(_cycle) => {
1248                // Cycles may be expected after CircularDependencyResolutionStage
1249                Ok(())
1250            }
1251        }
1252    }
1253
1254    fn validate_semantics(&mut self) -> Result<(), Vec<NormalizationError>> {
1255        // TODO: Add semantic validation passes
1256        if self.context.has_errors() {
1257            return Err(self.context.take_errors());
1258        }
1259        Ok(())
1260    }
1261
1262    fn build_semantic_ir(
1263        self,
1264        schema: &Schema,
1265        original_names: &HashMap<String, String>,
1266    ) -> Result<SemanticSchema, Vec<NormalizationError>> {
1267        let mut semantic_types = BTreeMap::new();
1268        let mut semantic_functions = BTreeMap::new();
1269
1270        let sorted_symbols = match self.context.symbol_table.topological_sort() {
1271            Ok(sorted) => sorted,
1272            Err(_cycle) => self.context.symbol_table.symbols.keys().cloned().collect(),
1273        };
1274
1275        for symbol_id in sorted_symbols {
1276            if let Some(raw_type) = self.context.raw_types.get(&symbol_id) {
1277                let semantic_type = self.build_semantic_type(raw_type, original_names)?;
1278                semantic_types.insert(symbol_id, semantic_type);
1279            }
1280        }
1281
1282        for (function_id, raw_function) in &self.context.raw_functions {
1283            let semantic_function = self.build_semantic_function(raw_function)?;
1284            semantic_functions.insert(function_id.clone(), semantic_function);
1285        }
1286
1287        Ok(SemanticSchema {
1288            id: schema.id.clone(),
1289            name: schema.name.clone(),
1290            description: schema.description.clone(),
1291            functions: semantic_functions,
1292            types: semantic_types,
1293            symbol_table: self.context.symbol_table,
1294        })
1295    }
1296
1297    fn build_semantic_type(
1298        &self,
1299        raw_type: &Type,
1300        original_names: &HashMap<String, String>,
1301    ) -> Result<SemanticType, Vec<NormalizationError>> {
1302        match raw_type {
1303            Type::Primitive(p) => Ok(SemanticType::Primitive(
1304                self.build_semantic_primitive(p, original_names)?,
1305            )),
1306            Type::Struct(s) => Ok(SemanticType::Struct(
1307                self.build_semantic_struct(s, original_names)?,
1308            )),
1309            Type::Enum(e) => Ok(SemanticType::Enum(
1310                self.build_semantic_enum(e, original_names)?,
1311            )),
1312        }
1313    }
1314
1315    fn build_semantic_primitive(
1316        &self,
1317        primitive: &Primitive,
1318        original_names: &HashMap<String, String>,
1319    ) -> Result<SemanticPrimitive, Vec<NormalizationError>> {
1320        let fallback = primitive
1321            .fallback
1322            .as_ref()
1323            .and_then(|tr| self.resolve_global_type_reference(&tr.name));
1324
1325        let original_name = original_names
1326            .get(&primitive.name)
1327            .cloned()
1328            .unwrap_or_else(|| primitive.name.clone());
1329
1330        Ok(SemanticPrimitive {
1331            id: primitive.id.clone(),
1332            name: primitive.name.clone(),
1333            original_name,
1334            description: primitive.description.clone(),
1335            parameters: primitive
1336                .parameters
1337                .iter()
1338                .map(|p| SemanticTypeParameter {
1339                    name: p.name.clone(),
1340                    description: p.description.clone(),
1341                    bounds: vec![],
1342                    default: None,
1343                })
1344                .collect(),
1345            fallback,
1346        })
1347    }
1348
1349    fn build_semantic_struct(
1350        &self,
1351        strukt: &Struct,
1352        original_names: &HashMap<String, String>,
1353    ) -> Result<SemanticStruct, Vec<NormalizationError>> {
1354        let mut fields = BTreeMap::new();
1355
1356        for field in strukt.fields() {
1357            let semantic_field = self.build_semantic_field(field)?;
1358            fields.insert(field.id.clone(), semantic_field);
1359        }
1360
1361        let original_name = original_names
1362            .get(&strukt.name)
1363            .cloned()
1364            .unwrap_or_else(|| strukt.name.clone());
1365
1366        Ok(SemanticStruct {
1367            id: strukt.id.clone(),
1368            name: strukt.name.clone(),
1369            original_name,
1370            serde_name: strukt.serde_name.clone(),
1371            description: strukt.description.clone(),
1372            parameters: strukt
1373                .parameters
1374                .iter()
1375                .map(|p| SemanticTypeParameter {
1376                    name: p.name.clone(),
1377                    description: p.description.clone(),
1378                    bounds: vec![],
1379                    default: None,
1380                })
1381                .collect(),
1382            fields,
1383            transparent: strukt.transparent,
1384            is_tuple: strukt.is_tuple(),
1385            is_unit: strukt.is_unit(),
1386            codegen_config: strukt.codegen_config.clone(),
1387        })
1388    }
1389
1390    fn build_semantic_enum(
1391        &self,
1392        enm: &Enum,
1393        original_names: &HashMap<String, String>,
1394    ) -> Result<SemanticEnum, Vec<NormalizationError>> {
1395        let mut variants = BTreeMap::new();
1396
1397        for variant in enm.variants() {
1398            let semantic_variant = self.build_semantic_variant(variant)?;
1399            variants.insert(variant.id.clone(), semantic_variant);
1400        }
1401
1402        let original_name = original_names
1403            .get(&enm.name)
1404            .cloned()
1405            .unwrap_or_else(|| enm.name.clone());
1406
1407        Ok(SemanticEnum {
1408            id: enm.id.clone(),
1409            name: enm.name.clone(),
1410            original_name,
1411            serde_name: enm.serde_name.clone(),
1412            description: enm.description.clone(),
1413            parameters: enm
1414                .parameters
1415                .iter()
1416                .map(|p| SemanticTypeParameter {
1417                    name: p.name.clone(),
1418                    description: p.description.clone(),
1419                    bounds: vec![],
1420                    default: None,
1421                })
1422                .collect(),
1423            variants,
1424            representation: enm.representation.clone(),
1425            codegen_config: enm.codegen_config.clone(),
1426        })
1427    }
1428
1429    fn build_semantic_field(
1430        &self,
1431        field: &Field,
1432    ) -> Result<SemanticField, Vec<NormalizationError>> {
1433        let resolved_type_ref = self.build_resolved_type_reference(&field.type_ref)?;
1434
1435        Ok(SemanticField {
1436            id: field.id.clone(),
1437            name: field.name.clone(),
1438            serde_name: field.serde_name.clone(),
1439            description: field.description.clone(),
1440            deprecation_note: field.deprecation_note.clone(),
1441            type_ref: resolved_type_ref,
1442            required: field.required,
1443            flattened: field.flattened,
1444            transform_callback: field.transform_callback.clone(),
1445        })
1446    }
1447
1448    fn build_semantic_variant(
1449        &self,
1450        variant: &Variant,
1451    ) -> Result<SemanticVariant, Vec<NormalizationError>> {
1452        let mut fields = BTreeMap::new();
1453
1454        for field in variant.fields() {
1455            let semantic_field = self.build_semantic_field(field)?;
1456            fields.insert(field.id.clone(), semantic_field);
1457        }
1458
1459        let field_style = match &variant.fields {
1460            Fields::Named(_) => FieldStyle::Named,
1461            Fields::Unnamed(_) => FieldStyle::Unnamed,
1462            Fields::None => FieldStyle::Unit,
1463        };
1464
1465        Ok(SemanticVariant {
1466            id: variant.id.clone(),
1467            name: variant.name.clone(),
1468            serde_name: variant.serde_name.clone(),
1469            description: variant.description.clone(),
1470            fields,
1471            discriminant: variant.discriminant,
1472            untagged: variant.untagged,
1473            field_style,
1474        })
1475    }
1476
1477    fn build_semantic_function(
1478        &self,
1479        function: &Function,
1480    ) -> Result<SemanticFunction, Vec<NormalizationError>> {
1481        let input_type = function
1482            .input_type
1483            .as_ref()
1484            .and_then(|tr| self.resolve_global_type_reference(&tr.name));
1485        let input_headers = function
1486            .input_headers
1487            .as_ref()
1488            .and_then(|tr| self.resolve_global_type_reference(&tr.name));
1489        let output_type = match &function.output_type {
1490            crate::OutputType::Complete { output_type } => SemanticOutputType::Complete(
1491                output_type
1492                    .as_ref()
1493                    .and_then(|tr| self.resolve_global_type_reference(&tr.name)),
1494            ),
1495            crate::OutputType::Stream { item_type } => SemanticOutputType::Stream {
1496                item_type: self
1497                    .resolve_global_type_reference(&item_type.name)
1498                    .ok_or_else(|| {
1499                        vec![NormalizationError::UnresolvedReference {
1500                            name: item_type.name.clone(),
1501                            referrer: function.id.clone(),
1502                        }]
1503                    })?,
1504            },
1505        };
1506        let error_type = function
1507            .error_type
1508            .as_ref()
1509            .and_then(|tr| self.resolve_global_type_reference(&tr.name));
1510
1511        Ok(SemanticFunction {
1512            id: function.id.clone(),
1513            name: function.name.clone(),
1514            path: function.path.clone(),
1515            description: function.description.clone(),
1516            deprecation_note: function.deprecation_note.clone(),
1517            input_type,
1518            input_headers,
1519            output_type,
1520            error_type,
1521            serialization: function.serialization.clone(),
1522            readonly: function.readonly,
1523            tags: function.tags.clone(),
1524        })
1525    }
1526
1527    fn build_resolved_type_reference(
1528        &self,
1529        type_ref: &TypeReference,
1530    ) -> Result<ResolvedTypeReference, Vec<NormalizationError>> {
1531        let is_likely_generic = !type_ref.name.contains("::");
1532
1533        let target =
1534            if let Some(target) = self.context.resolution_cache.get(&type_ref.name).cloned() {
1535                target
1536            } else if is_likely_generic {
1537                SymbolId::new(SymbolKind::TypeAlias, vec![type_ref.name.clone()])
1538            } else {
1539                SymbolId::new(SymbolKind::Struct, vec![type_ref.name.replace("::", "_")])
1540            };
1541
1542        let mut resolved_args = Vec::new();
1543        for arg in &type_ref.arguments {
1544            resolved_args.push(self.build_resolved_type_reference(arg)?);
1545        }
1546
1547        Ok(ResolvedTypeReference::new(
1548            target,
1549            resolved_args,
1550            type_ref.name.clone(),
1551        ))
1552    }
1553}
1554
1555impl Default for Normalizer {
1556    fn default() -> Self {
1557        Self::new()
1558    }
1559}
1560
1561// ---------------------------------------------------------------------------
1562// Tests
1563// ---------------------------------------------------------------------------
1564
1565#[cfg(test)]
1566mod tests {
1567    use super::*;
1568    use crate::{Fields, Function, Representation, Schema, Struct, TypeReference, Typespace};
1569
1570    #[test]
1571    fn test_basic_normalization() {
1572        let mut schema = Schema::new();
1573        schema.name = "TestSchema".to_string();
1574
1575        let user_struct = Struct::new("User");
1576        let user_type = Type::Struct(user_struct);
1577
1578        let mut input_types = Typespace::new();
1579        input_types.insert_type(user_type);
1580        schema.input_types = input_types;
1581
1582        let normalizer = Normalizer::new();
1583        let result = normalizer.normalize(&schema);
1584
1585        assert!(
1586            result.is_ok(),
1587            "Normalization should succeed for simple schema"
1588        );
1589
1590        let semantic_schema = result.unwrap();
1591        assert_eq!(semantic_schema.name, "TestSchema");
1592        assert_eq!(semantic_schema.types.len(), 1);
1593    }
1594
1595    #[test]
1596    fn test_unresolved_reference_handled_gracefully() {
1597        let mut schema = Schema::new();
1598        schema.name = "TestSchema".to_string();
1599
1600        let mut function = Function::new("test_function".to_string());
1601        function.input_type = Some(TypeReference::new("NonExistentType", vec![]));
1602        schema.functions.push(function);
1603
1604        let normalizer = Normalizer::new();
1605        let result = normalizer.normalize(&schema);
1606
1607        assert!(
1608            result.is_ok(),
1609            "Normalization should handle unresolved references gracefully"
1610        );
1611
1612        let semantic_schema = result.unwrap();
1613        assert!(!semantic_schema.functions.is_empty());
1614    }
1615
1616    #[test]
1617    fn test_normalize_with_functions_and_types() {
1618        let mut schema = Schema::new();
1619        schema.name = "API".to_string();
1620
1621        // Add types
1622        let mut user_struct = Struct::new("api::User");
1623        user_struct.fields = Fields::Named(vec![
1624            Field::new("name".into(), "std::string::String".into()),
1625            Field::new("age".into(), "u32".into()),
1626        ]);
1627        schema.input_types.insert_type(user_struct.into());
1628
1629        let mut error_enum = Enum::new("api::Error".into());
1630        error_enum.representation = Representation::Internal { tag: "type".into() };
1631        error_enum.variants = vec![
1632            Variant::new("NotFound".into()),
1633            Variant::new("Forbidden".into()),
1634        ];
1635        schema.output_types.insert_type(error_enum.into());
1636
1637        // Add a function referencing both types
1638        let mut function = Function::new("get_user".into());
1639        function.input_type = Some(TypeReference::new("api::User", vec![]));
1640        function.error_type = Some(TypeReference::new("api::Error", vec![]));
1641        schema.functions.push(function);
1642
1643        let normalizer = Normalizer::new();
1644        let result = normalizer.normalize(&schema);
1645        assert!(result.is_ok(), "Normalization failed: {:?}", result.err());
1646
1647        let semantic = result.unwrap();
1648        assert_eq!(semantic.types.len(), 2);
1649        assert_eq!(semantic.functions.len(), 1);
1650
1651        // Verify the function has resolved type references
1652        let func = semantic.functions.values().next().unwrap();
1653        assert!(func.input_type.is_some());
1654        assert!(func.error_type.is_some());
1655    }
1656
1657    #[test]
1658    fn test_normalize_function_with_input_headers() {
1659        let mut schema = Schema::new();
1660        schema.name = "API".to_string();
1661
1662        let headers_struct = Struct::new("Headers");
1663        schema.input_types.insert_type(headers_struct.into());
1664
1665        let body_struct = Struct::new("Body");
1666        schema.input_types.insert_type(body_struct.into());
1667
1668        let mut function = Function::new("do_thing".into());
1669        function.input_type = Some(TypeReference::new("Body", vec![]));
1670        function.input_headers = Some(TypeReference::new("Headers", vec![]));
1671        schema.functions.push(function);
1672
1673        let normalizer = Normalizer::new();
1674        let semantic = normalizer.normalize(&schema).unwrap();
1675
1676        let func = semantic.functions.values().next().unwrap();
1677        assert!(func.input_type.is_some());
1678        assert!(func.input_headers.is_some());
1679    }
1680
1681    #[test]
1682    fn test_type_consolidation_shared_name() {
1683        let mut schema = Schema::new();
1684        schema.name = "Test".to_string();
1685
1686        // Same simple name in both typespaces triggers conflict renaming
1687        let input_struct = Struct::new("Shared");
1688        let output_struct = Struct::new("Shared");
1689        schema.input_types.insert_type(input_struct.into());
1690        schema.output_types.insert_type(output_struct.into());
1691
1692        let stage = TypeConsolidationStage;
1693        stage.transform(&mut schema).unwrap();
1694
1695        // Both get prefixed since they share a simple name
1696        let type_names: Vec<_> = schema
1697            .input_types
1698            .types()
1699            .map(|t| t.name().to_string())
1700            .collect();
1701        assert!(
1702            type_names.contains(&"input.Shared".to_string()),
1703            "Expected input.Shared, got: {type_names:?}"
1704        );
1705        assert!(
1706            type_names.contains(&"output.Shared".to_string()),
1707            "Expected output.Shared, got: {type_names:?}"
1708        );
1709        assert!(schema.output_types.is_empty());
1710    }
1711
1712    #[test]
1713    fn test_type_consolidation_conflict_renaming() {
1714        let mut schema = Schema::new();
1715        schema.name = "Test".to_string();
1716
1717        // Different types sharing simple name get renamed
1718        let mut input_struct = Struct::new("Foo");
1719        input_struct.description = "input version".into();
1720        let mut output_struct = Struct::new("Foo");
1721        output_struct.description = "output version".into();
1722        // Make them different so they're not deduplicated
1723        output_struct.fields = Fields::Named(vec![Field::new("x".into(), "u32".into())]);
1724
1725        schema.input_types.insert_type(input_struct.into());
1726        schema.output_types.insert_type(output_struct.into());
1727
1728        let stage = TypeConsolidationStage;
1729        stage.transform(&mut schema).unwrap();
1730
1731        let type_names: Vec<_> = schema
1732            .input_types
1733            .types()
1734            .map(|t| t.name().to_string())
1735            .collect();
1736        assert!(
1737            type_names.contains(&"input.Foo".to_string())
1738                || type_names.contains(&"output.Foo".to_string()),
1739            "Expected conflict renaming, got: {type_names:?}"
1740        );
1741    }
1742
1743    #[test]
1744    fn test_ensure_symbol_ids_idempotent() {
1745        let mut schema = Schema::new();
1746        schema.name = "Test".to_string();
1747
1748        let mut user_struct = Struct::new("User");
1749        user_struct.fields = Fields::Named(vec![Field::new("id".into(), "u64".into())]);
1750        schema.input_types.insert_type(user_struct.into());
1751
1752        // Run twice
1753        crate::ensure_symbol_ids(&mut schema);
1754        let ids_first: Vec<_> = schema
1755            .input_types
1756            .types()
1757            .map(|t| match t {
1758                Type::Struct(s) => s.id.clone(),
1759                _ => unreachable!(),
1760            })
1761            .collect();
1762
1763        crate::ensure_symbol_ids(&mut schema);
1764        let ids_second: Vec<_> = schema
1765            .input_types
1766            .types()
1767            .map(|t| match t {
1768                Type::Struct(s) => s.id.clone(),
1769                _ => unreachable!(),
1770            })
1771            .collect();
1772
1773        assert_eq!(
1774            ids_first, ids_second,
1775            "ensure_symbol_ids should be idempotent"
1776        );
1777    }
1778
1779    #[test]
1780    fn test_ensure_symbol_ids_enum_variants_and_fields() {
1781        let mut schema = Schema::new();
1782        schema.name = "Test".to_string();
1783
1784        let mut enm = Enum::new("Status".into());
1785        let mut variant = Variant::new("Active".into());
1786        variant.fields = Fields::Named(vec![Field::new(
1787            "since".into(),
1788            "std::string::String".into(),
1789        )]);
1790        enm.variants = vec![variant, Variant::new("Inactive".into())];
1791        schema.input_types.insert_type(enm.into());
1792
1793        crate::ensure_symbol_ids(&mut schema);
1794
1795        let enm = schema
1796            .input_types
1797            .get_type("Status")
1798            .unwrap()
1799            .as_enum()
1800            .unwrap();
1801        assert!(!enm.id.is_unknown(), "Enum should have a non-unknown id");
1802
1803        for variant in &enm.variants {
1804            assert!(
1805                !variant.id.is_unknown(),
1806                "Variant '{}' should have a non-unknown id",
1807                variant.name
1808            );
1809            for field in variant.fields() {
1810                assert!(
1811                    !field.id.is_unknown(),
1812                    "Field '{}' in variant '{}' should have a non-unknown id",
1813                    field.name,
1814                    variant.name
1815                );
1816            }
1817        }
1818
1819        // Check paths are structured correctly
1820        let active = &enm.variants[0];
1821        assert_eq!(active.id.path.last().unwrap(), "Active");
1822        let since_field = active.fields().next().unwrap();
1823        assert!(
1824            since_field.id.path.contains(&"Active".to_string()),
1825            "Field path should include parent variant: {:?}",
1826            since_field.id.path
1827        );
1828    }
1829
1830    #[test]
1831    fn test_circular_dependency_detection() {
1832        let mut schema = Schema::new();
1833        schema.name = "Test".to_string();
1834
1835        // Node { children: Vec<Node> } - self-referential
1836        let mut node_struct = Struct::new("Node");
1837        node_struct.fields = Fields::Named(vec![Field::new(
1838            "children".into(),
1839            TypeReference::new("std::vec::Vec", vec![TypeReference::new("Node", vec![])]),
1840        )]);
1841        schema.input_types.insert_type(node_struct.into());
1842
1843        let stage = CircularDependencyResolutionStage::new();
1844        // Should detect the cycle but not fail (strategies are stubs)
1845        let result = stage.transform(&mut schema);
1846        assert!(result.is_ok());
1847    }
1848
1849    #[test]
1850    fn test_empty_schema_normalization() {
1851        let schema = Schema::new();
1852        let normalizer = Normalizer::new();
1853        let result = normalizer.normalize(&schema);
1854        assert!(result.is_ok());
1855
1856        let semantic = result.unwrap();
1857        assert!(semantic.types.is_empty());
1858        assert!(semantic.functions.is_empty());
1859    }
1860
1861    #[test]
1862    fn test_naming_resolution_all_conflicting_types_have_references_rewritten() {
1863        // Regression: NamingResolutionStage only tracked the first qualified name
1864        // per simple name in name_usage, leaving references to the second conflicting
1865        // type dangling after rename.
1866        let mut schema = Schema::new();
1867        schema.name = "Test".to_string();
1868
1869        // Two types sharing simple name "Foo" in different modules
1870        let a_foo = Struct::new("a::Foo");
1871        let b_foo = Struct::new("b::Foo");
1872        schema.input_types.insert_type(a_foo.into());
1873        schema.input_types.insert_type(b_foo.into());
1874
1875        // Function referencing BOTH types
1876        let mut func1 = Function::new("use_a_foo".into());
1877        func1.input_type = Some(TypeReference::new("a::Foo", vec![]));
1878        schema.functions.push(func1);
1879
1880        let mut func2 = Function::new("use_b_foo".into());
1881        func2.input_type = Some(TypeReference::new("b::Foo", vec![]));
1882        schema.functions.push(func2);
1883
1884        let stage = NamingResolutionStage;
1885        stage.transform(&mut schema).unwrap();
1886
1887        // Collect all type names defined in the schema
1888        let type_names: std::collections::HashSet<String> = schema
1889            .input_types
1890            .types()
1891            .map(|t| t.name().to_string())
1892            .collect();
1893
1894        // Both function references must point to names that exist in the schema
1895        for func in &schema.functions {
1896            if let Some(ref input_type) = func.input_type {
1897                assert!(
1898                    type_names.contains(&input_type.name),
1899                    "Function '{}' references type '{}' which doesn't exist in schema. Available: {:?}",
1900                    func.name, input_type.name, type_names
1901                );
1902            }
1903        }
1904    }
1905
1906    #[test]
1907    fn test_generate_unique_name_excluded_modules_no_collision() {
1908        // Regression: when all module parts are in the exclusion list ("model", "proto"),
1909        // the fallback was module_parts[0], causing "model::Foo" and "model::proto::Foo"
1910        // to both become "ModelFoo". Now uses joined fallback to avoid collisions.
1911        let name1 = generate_unique_name("model::Foo");
1912        let name2 = generate_unique_name("model::proto::Foo");
1913
1914        assert_ne!(
1915            name1, name2,
1916            "model::Foo and model::proto::Foo must produce different names, got '{name1}' and '{name2}'"
1917        );
1918    }
1919
1920    #[test]
1921    fn test_generate_unique_name_with_non_excluded_module() {
1922        // Normal case: module part not in exclusion list is used as prefix
1923        let name = generate_unique_name("billing::Invoice");
1924        assert_eq!(name, "BillingInvoice");
1925    }
1926
1927    #[test]
1928    fn test_self_referential_type_normalizes_successfully() {
1929        // A self-referential type (cycle of length 1) should pass through the
1930        // full Normalizer pipeline without error.  In Rust the schema already
1931        // records Box<T> wrappers, so the boxing strategy is intentionally a
1932        // no-op — the cycle is detected but does not block normalization.
1933        let mut schema = Schema::new();
1934        schema.name = "TreeSchema".to_string();
1935
1936        // TreeNode has a field `children` of type Vec<TreeNode> (indirect
1937        // self-reference via a container — already broken by Vec) and a field
1938        // `parent` that directly references TreeNode (direct self-reference,
1939        // which in real Rust code would be Box<TreeNode>).
1940        let mut tree_node = Struct::new("TreeNode");
1941        tree_node.fields = Fields::Named(vec![
1942            Field::new("label".into(), "std::string::String".into()),
1943            Field::new(
1944                "children".into(),
1945                TypeReference::new(
1946                    "std::vec::Vec",
1947                    vec![TypeReference::new("TreeNode", vec![])],
1948                ),
1949            ),
1950            Field::new(
1951                "parent".into(),
1952                TypeReference::new(
1953                    "std::boxed::Box",
1954                    vec![TypeReference::new("TreeNode", vec![])],
1955                ),
1956            ),
1957        ]);
1958        schema.input_types.insert_type(tree_node.into());
1959
1960        let normalizer = Normalizer::new();
1961        let result = normalizer.normalize(&schema);
1962
1963        assert!(
1964            result.is_ok(),
1965            "Self-referential type should not prevent normalization: {:?}",
1966            result.err()
1967        );
1968
1969        let semantic = result.unwrap();
1970        assert_eq!(semantic.types.len(), 1, "TreeNode type should be present");
1971
1972        // Verify the type round-tripped with the expected name
1973        let tree_node_type = semantic.types.values().next().unwrap();
1974        match tree_node_type {
1975            SemanticType::Struct(s) => {
1976                assert_eq!(s.name, "TreeNode");
1977                assert_eq!(s.fields.len(), 3, "All three fields should survive");
1978            }
1979            other => panic!("Expected Struct, got {:?}", std::mem::discriminant(other)),
1980        }
1981    }
1982
1983    #[test]
1984    fn test_multi_type_cycle_normalizes_successfully() {
1985        // A → B → A cycle (length 2) should also pass through normalization
1986        // without error.  The forward-declaration strategy is likewise a no-op
1987        // for Rust schemas.
1988        let mut schema = Schema::new();
1989        schema.name = "CycleSchema".to_string();
1990
1991        // Department references Employee, Employee references Department
1992        let mut department = Struct::new("Department");
1993        department.fields = Fields::Named(vec![
1994            Field::new("name".into(), "std::string::String".into()),
1995            Field::new("manager".into(), TypeReference::new("Employee", vec![])),
1996        ]);
1997
1998        let mut employee = Struct::new("Employee");
1999        employee.fields = Fields::Named(vec![
2000            Field::new("name".into(), "std::string::String".into()),
2001            Field::new(
2002                "department".into(),
2003                TypeReference::new("Department", vec![]),
2004            ),
2005        ]);
2006
2007        schema.input_types.insert_type(department.into());
2008        schema.input_types.insert_type(employee.into());
2009
2010        let normalizer = Normalizer::new();
2011        let result = normalizer.normalize(&schema);
2012
2013        assert!(
2014            result.is_ok(),
2015            "Multi-type cycle should not prevent normalization: {:?}",
2016            result.err()
2017        );
2018
2019        let semantic = result.unwrap();
2020        assert_eq!(
2021            semantic.types.len(),
2022            2,
2023            "Both Department and Employee types should be present"
2024        );
2025    }
2026
2027    #[test]
2028    fn test_type_consolidation_qualified_name_uniqueness() {
2029        // Regression: when input types `a::Foo` and `b::Foo` both conflict with
2030        // an output type `c::Foo`, all three must receive distinct names after
2031        // consolidation — no silent drops.
2032        let mut schema = Schema::new();
2033        schema.name = "Test".to_string();
2034
2035        let a_foo = Struct::new("a::Foo");
2036        let b_foo = Struct::new("b::Foo");
2037        let c_foo = Struct::new("c::Foo");
2038
2039        schema.input_types.insert_type(a_foo.into());
2040        schema.input_types.insert_type(b_foo.into());
2041        schema.output_types.insert_type(c_foo.into());
2042
2043        let stage = TypeConsolidationStage;
2044        stage.transform(&mut schema).unwrap();
2045
2046        let type_names: Vec<String> = schema
2047            .input_types
2048            .types()
2049            .map(|t| t.name().to_string())
2050            .collect();
2051
2052        // All three should be present with distinct names
2053        assert_eq!(
2054            type_names.len(),
2055            3,
2056            "All three Foo types should survive consolidation, got: {type_names:?}"
2057        );
2058
2059        // Verify uniqueness — no two names are the same
2060        let unique_names: std::collections::HashSet<&String> = type_names.iter().collect();
2061        assert_eq!(
2062            unique_names.len(),
2063            3,
2064            "All three names should be distinct, got: {type_names:?}"
2065        );
2066
2067        // Verify the naming convention: input types get "input." prefix,
2068        // output types get "output." prefix
2069        let has_input_a = type_names
2070            .iter()
2071            .any(|n| n.contains("input") && n.contains("a"));
2072        let has_input_b = type_names
2073            .iter()
2074            .any(|n| n.contains("input") && n.contains("b"));
2075        let has_output_c = type_names
2076            .iter()
2077            .any(|n| n.contains("output") && n.contains("c"));
2078        assert!(
2079            has_input_a,
2080            "Expected an input.a.Foo variant, got: {type_names:?}"
2081        );
2082        assert!(
2083            has_input_b,
2084            "Expected an input.b.Foo variant, got: {type_names:?}"
2085        );
2086        assert!(
2087            has_output_c,
2088            "Expected an output.c.Foo variant, got: {type_names:?}"
2089        );
2090    }
2091
2092    #[test]
2093    fn test_resolve_types_does_not_confuse_variant_with_type() {
2094        // Regression: the resolve_types phase should resolve a function's type
2095        // reference "Status" to the Struct named "Status", not to an enum variant
2096        // that happens to also be named "Status".
2097        let mut schema = Schema::new();
2098        schema.name = "Test".to_string();
2099
2100        // A struct named "Status"
2101        let status_struct = Struct::new("Status");
2102        schema.input_types.insert_type(status_struct.into());
2103
2104        // An enum with a variant named "Status"
2105        let mut state_enum = Enum::new("State".into());
2106        state_enum.variants = vec![Variant::new("Status".into()), Variant::new("Error".into())];
2107        schema.input_types.insert_type(state_enum.into());
2108
2109        // A function that references "Status" — should resolve to the Struct
2110        let mut function = Function::new("get_status".into());
2111        function.input_type = Some(TypeReference::new("Status", vec![]));
2112        schema.functions.push(function);
2113
2114        let normalizer = Normalizer::new();
2115        let result = normalizer.normalize(&schema);
2116        assert!(
2117            result.is_ok(),
2118            "Normalization should succeed: {:?}",
2119            result.err()
2120        );
2121
2122        let semantic = result.unwrap();
2123        let func = semantic.functions.values().next().unwrap();
2124
2125        // The function's input_type should resolve to the Status struct's ID
2126        let resolved_id = func
2127            .input_type
2128            .as_ref()
2129            .expect("input_type should be resolved");
2130
2131        // It should be a Struct kind, not a Variant kind
2132        assert_eq!(
2133            resolved_id.kind,
2134            crate::SymbolKind::Struct,
2135            "Function's input_type should resolve to a Struct, not a Variant. Got: {resolved_id:?}"
2136        );
2137    }
2138
2139    #[test]
2140    fn test_generate_unique_name_same_inner_module() {
2141        // Regression: two types with the same inner module and type name but
2142        // different outer modules must produce different unique names.
2143        let name_a = generate_unique_name("services::user::Profile");
2144        let name_b = generate_unique_name("auth::user::Profile");
2145
2146        assert_ne!(
2147            name_a, name_b,
2148            "services::user::Profile and auth::user::Profile must produce different names, \
2149             got '{name_a}' and '{name_b}'"
2150        );
2151
2152        // Verify they follow the expected PascalCase convention
2153        assert!(
2154            name_a.contains("Services") || name_a.contains("services"),
2155            "Expected 'services' component in name, got '{name_a}'"
2156        );
2157        assert!(
2158            name_b.contains("Auth") || name_b.contains("auth"),
2159            "Expected 'auth' component in name, got '{name_b}'"
2160        );
2161    }
2162
2163    #[test]
2164    fn test_function_symbol_path_matches_id() {
2165        // Regression: after normalization, a function's SymbolId should be
2166        // retrievable from the symbol table via its path.
2167        let mut schema = Schema::new();
2168        schema.name = "API".to_string();
2169
2170        let mut function = Function::new("get_user".into());
2171        function.input_type = None;
2172        function.output_type = crate::OutputType::Complete { output_type: None };
2173        schema.functions.push(function);
2174
2175        let normalizer = Normalizer::new();
2176        let semantic = normalizer
2177            .normalize(&schema)
2178            .expect("Normalization should succeed");
2179
2180        // Get the function's ID
2181        let (function_id, _) = semantic.functions.iter().next().unwrap();
2182
2183        // Verify the symbol table can find it by path
2184        let found = semantic.symbol_table.get_by_path(&function_id.path);
2185        assert!(
2186            found.is_some(),
2187            "symbol_table.get_by_path({:?}) should return Some, but got None. \
2188             Function ID: {function_id:?}",
2189            function_id.path
2190        );
2191
2192        let symbol_info = found.unwrap();
2193        assert_eq!(
2194            symbol_info.kind,
2195            crate::SymbolKind::Endpoint,
2196            "Symbol should be an Endpoint, got {:?}",
2197            symbol_info.kind
2198        );
2199    }
2200}