sea_core/projection/
protobuf.rs

1//! Protobuf Projection Engine
2//!
3//! This module provides functionality to project SEA semantic graphs to Protocol Buffer
4//! (`.proto`) files. It supports:
5//!
6//! - Entity and Resource projection to Protobuf messages
7//! - Type mapping from SEA types to Protobuf scalar types
8//! - Deterministic field numbering for schema stability
9//! - Governance message generation
10//!
11//! # Example
12//!
13//! ```rust,ignore
14//! use sea_core::projection::protobuf::ProtobufEngine;
15//! use sea_core::graph::Graph;
16//!
17//! let graph = build_graph_from_model();
18//! let proto_file = ProtobufEngine::project(&graph, "my_namespace", "my.package");
19//! println!("{}", proto_file.to_proto_string());
20//! ```
21
22use crate::graph::Graph;
23use crate::primitives::{Entity, Resource};
24use serde::{Deserialize, Serialize};
25use serde_json::Value;
26use std::collections::{BTreeMap, HashMap, HashSet};
27use std::path::PathBuf;
28
29// ============================================================================
30// Protobuf IR Types
31// ============================================================================
32
33/// Represents a complete `.proto` file.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProtoFile {
36    /// The package name (e.g., "sea.example")
37    pub package: String,
38    /// Protobuf syntax version (always "proto3")
39    pub syntax: String,
40    /// Import statements
41    pub imports: Vec<String>,
42    /// File-level options
43    pub options: ProtoOptions,
44    /// Enum definitions
45    pub enums: Vec<ProtoEnum>,
46    /// Message definitions
47    pub messages: Vec<ProtoMessage>,
48    /// gRPC service definitions
49    pub services: Vec<ProtoService>,
50    /// Metadata about the projection
51    pub metadata: ProtoMetadata,
52}
53
54impl ProtoFile {
55    /// Create a new ProtoFile with the given package name.
56    pub fn new(package: impl Into<String>) -> Self {
57        Self {
58            package: package.into(),
59            syntax: "proto3".to_string(),
60            imports: Vec::new(),
61            options: ProtoOptions::default(),
62            enums: Vec::new(),
63            messages: Vec::new(),
64            services: Vec::new(),
65            metadata: ProtoMetadata::default(),
66        }
67    }
68
69    /// Serialize the ProtoFile to `.proto` text format.
70    pub fn to_proto_string(&self) -> String {
71        let mut out = String::new();
72
73        // Header comments
74        out.push_str("// Generated by SEA Projection Framework\n");
75        out.push_str(&format!(
76            "// Projection: {}\n",
77            self.metadata.projection_name
78        ));
79        out.push_str(&format!(
80            "// Source Namespace: {}\n",
81            self.metadata.source_namespace
82        ));
83        if let Some(ref version) = self.metadata.semantic_version {
84            out.push_str(&format!("// Version: {}\n", version));
85        }
86        out.push_str(&format!(
87            "// Generated At: {}\n",
88            self.metadata.generated_at
89        ));
90        out.push_str("// DO NOT EDIT - This file is auto-generated\n\n");
91
92        // Syntax
93        out.push_str(&format!("syntax = \"{}\";\n\n", self.syntax));
94
95        // Package
96        out.push_str(&format!("package {};\n", self.package));
97
98        // Options
99        let options = &self.options;
100
101        if let Some(ref pkg) = options.java_package {
102            out.push_str(&format!("\noption java_package = \"{}\";", pkg));
103        }
104        if options.java_multiple_files {
105            out.push_str("\noption java_multiple_files = true;");
106        }
107        if let Some(ref pkg) = options.go_package {
108            out.push_str(&format!("\noption go_package = \"{}\";", pkg));
109        }
110        if let Some(ref ns) = options.csharp_namespace {
111            out.push_str(&format!("\noption csharp_namespace = \"{}\";", ns));
112        }
113        if let Some(ref ns) = options.php_namespace {
114            out.push_str(&format!("\noption php_namespace = \"{}\";", ns));
115        }
116        if let Some(ref pkg) = options.ruby_package {
117            out.push_str(&format!("\noption ruby_package = \"{}\";", pkg));
118        }
119        if let Some(ref prefix) = options.swift_prefix {
120            out.push_str(&format!("\noption swift_prefix = \"{}\";", prefix));
121        }
122        if let Some(ref prefix) = options.objc_class_prefix {
123            out.push_str(&format!("\noption objc_class_prefix = \"{}\";", prefix));
124        }
125        if let Some(ref opt) = options.optimize_for {
126            out.push_str(&format!("\noption optimize_for = {};", opt));
127        }
128        if options.deprecated {
129            out.push_str("\noption deprecated = true;");
130        }
131
132        // Custom options
133        for custom in &options.custom_options {
134            out.push_str(&format!("\n{}", custom.to_proto_string()));
135        }
136
137        if options.java_package.is_some()
138            || options.java_multiple_files
139            || options.go_package.is_some()
140            || options.csharp_namespace.is_some()
141            || options.php_namespace.is_some()
142            || options.ruby_package.is_some()
143            || options.swift_prefix.is_some()
144            || options.objc_class_prefix.is_some()
145            || options.optimize_for.is_some()
146            || options.deprecated
147            || !options.custom_options.is_empty()
148        {
149            out.push('\n');
150        }
151
152        // Imports
153        if !self.imports.is_empty() {
154            out.push('\n');
155            for import in &self.imports {
156                out.push_str(&format!("import \"{}\";\n", import));
157            }
158        }
159
160        // Enums
161        for e in &self.enums {
162            out.push('\n');
163            out.push_str(&e.to_proto_string());
164        }
165
166        // Messages
167        for m in &self.messages {
168            out.push('\n');
169            out.push_str(&m.to_proto_string());
170        }
171
172        // Services (gRPC)
173        for s in &self.services {
174            out.push('\n');
175            out.push_str(&s.to_proto_string());
176        }
177
178        out
179    }
180
181    /// Scan all messages and automatically add required Well-Known Type imports.
182    ///
183    /// This method should be called after all messages are added to ensure
184    /// proper imports are included for any WKT fields.
185    pub fn add_wkt_imports(&mut self) {
186        use std::collections::HashSet;
187        let mut required_imports: HashSet<&'static str> = HashSet::new();
188
189        // Scan all message fields for WKT references
190        for msg in &self.messages {
191            Self::collect_wkt_imports_from_message(msg, &mut required_imports);
192        }
193
194        // Add imports that aren't already present
195        for import in required_imports {
196            if !self.imports.contains(&import.to_string()) {
197                self.imports.push(import.to_string());
198            }
199        }
200
201        // Sort imports for deterministic output
202        self.imports.sort();
203    }
204
205    fn collect_wkt_imports_from_message(
206        msg: &ProtoMessage,
207        imports: &mut std::collections::HashSet<&'static str>,
208    ) {
209        for field in &msg.fields {
210            if let ProtoType::Message(ref type_name) = field.proto_type {
211                if let Some(wkt) = WellKnownType::from_type_name(type_name) {
212                    imports.insert(wkt.import_path());
213                }
214            }
215        }
216
217        // Recurse into nested messages
218        for nested in &msg.nested_messages {
219            Self::collect_wkt_imports_from_message(nested, imports);
220        }
221    }
222}
223
224/// File-level Protobuf options.
225#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226pub struct ProtoOptions {
227    /// Java package for generated code
228    pub java_package: Option<String>,
229    /// Generate separate files for each message in Java
230    pub java_multiple_files: bool,
231    /// Go package path
232    pub go_package: Option<String>,
233    /// C# namespace
234    pub csharp_namespace: Option<String>,
235    /// PHP namespace
236    pub php_namespace: Option<String>,
237    /// Ruby package
238    pub ruby_package: Option<String>,
239    /// Swift prefix
240    pub swift_prefix: Option<String>,
241    /// Objective-C class prefix
242    pub objc_class_prefix: Option<String>,
243    /// Optimize for: SPEED, CODE_SIZE, or LITE_RUNTIME
244    pub optimize_for: Option<String>,
245    /// Mark all messages as deprecated
246    pub deprecated: bool,
247    /// Custom options (user-defined or extension options)
248    pub custom_options: Vec<ProtoCustomOption>,
249}
250
251impl ProtoOptions {
252    /// Set a standard option by name.
253    pub fn set_option(&mut self, name: &str, value: ProtoOptionValue) {
254        match name {
255            "java_package" => {
256                if let ProtoOptionValue::String(s) = value {
257                    self.java_package = Some(s);
258                }
259            }
260            "java_multiple_files" => {
261                if let ProtoOptionValue::Bool(b) = value {
262                    self.java_multiple_files = b;
263                }
264            }
265            "go_package" => {
266                if let ProtoOptionValue::String(s) = value {
267                    self.go_package = Some(s);
268                }
269            }
270            "csharp_namespace" => {
271                if let ProtoOptionValue::String(s) = value {
272                    self.csharp_namespace = Some(s);
273                }
274            }
275            "php_namespace" => {
276                if let ProtoOptionValue::String(s) = value {
277                    self.php_namespace = Some(s);
278                }
279            }
280            "ruby_package" => {
281                if let ProtoOptionValue::String(s) = value {
282                    self.ruby_package = Some(s);
283                }
284            }
285            "swift_prefix" => {
286                if let ProtoOptionValue::String(s) = value {
287                    self.swift_prefix = Some(s);
288                }
289            }
290            "objc_class_prefix" => {
291                if let ProtoOptionValue::String(s) = value {
292                    self.objc_class_prefix = Some(s);
293                }
294            }
295            "optimize_for" => {
296                if let ProtoOptionValue::String(s) = value {
297                    self.optimize_for = Some(s);
298                }
299            }
300            "deprecated" => {
301                if let ProtoOptionValue::Bool(b) = value {
302                    self.deprecated = b;
303                }
304            }
305            _ => {
306                // Unknown option, add as custom
307                self.custom_options.push(ProtoCustomOption {
308                    name: name.to_string(),
309                    value,
310                });
311            }
312        }
313    }
314}
315
316/// A custom proto option (user-defined or extension).
317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
318pub struct ProtoCustomOption {
319    /// Option name (e.g., "java_package" or "(myopt).field")
320    pub name: String,
321    /// Option value
322    pub value: ProtoOptionValue,
323}
324
325impl ProtoCustomOption {
326    /// Create a new custom option.
327    pub fn new(name: impl Into<String>, value: ProtoOptionValue) -> Self {
328        Self {
329            name: name.into(),
330            value,
331        }
332    }
333
334    /// Serialize to proto option string.
335    pub fn to_proto_string(&self) -> String {
336        format!("option {} = {};", self.name, self.value.to_proto_string())
337    }
338}
339
340/// Value for a proto option.
341#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
342pub enum ProtoOptionValue {
343    /// String value
344    String(String),
345    /// Integer value
346    Int(i64),
347    /// Float value
348    Float(f64),
349    /// Boolean value
350    Bool(bool),
351    /// Identifier/enum value (unquoted)
352    Identifier(String),
353}
354
355impl ProtoOptionValue {
356    /// Parse option value from JSON Value.
357    pub fn from_json(value: &serde_json::Value) -> Self {
358        match value {
359            serde_json::Value::String(s) => ProtoOptionValue::String(s.clone()),
360            serde_json::Value::Bool(b) => ProtoOptionValue::Bool(*b),
361            serde_json::Value::Number(n) => {
362                if let Some(i) = n.as_i64() {
363                    ProtoOptionValue::Int(i)
364                } else if let Some(f) = n.as_f64() {
365                    ProtoOptionValue::Float(f)
366                } else {
367                    ProtoOptionValue::String(n.to_string())
368                }
369            }
370            _ => ProtoOptionValue::String(value.to_string()),
371        }
372    }
373
374    /// Serialize to proto option value string.
375    pub fn to_proto_string(&self) -> String {
376        match self {
377            ProtoOptionValue::String(s) => {
378                format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
379            }
380            ProtoOptionValue::Int(i) => i.to_string(),
381            ProtoOptionValue::Float(f) => f.to_string(),
382            ProtoOptionValue::Bool(b) => b.to_string(),
383            ProtoOptionValue::Identifier(s) => s.clone(),
384        }
385    }
386}
387
388/// Metadata about the projection source.
389#[derive(Debug, Clone, Default, Serialize, Deserialize)]
390pub struct ProtoMetadata {
391    /// Name of the projection that generated this file
392    pub projection_name: String,
393    /// Semantic version if available
394    pub semantic_version: Option<String>,
395    /// Source namespace from the SEA model
396    pub source_namespace: String,
397    /// Timestamp of generation
398    pub generated_at: String,
399}
400
401// ============================================================================
402// gRPC Service Types
403// ============================================================================
404
405/// Represents a gRPC service definition.
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ProtoService {
408    /// Service name (e.g., "PaymentProcessorService")
409    pub name: String,
410    /// RPC methods in this service
411    pub methods: Vec<ProtoRpcMethod>,
412    /// Documentation comments
413    pub comments: Vec<String>,
414}
415
416impl ProtoService {
417    /// Create a new ProtoService with the given name.
418    pub fn new(name: impl Into<String>) -> Self {
419        Self {
420            name: name.into(),
421            methods: Vec::new(),
422            comments: Vec::new(),
423        }
424    }
425
426    /// Serialize the service to `.proto` text format.
427    pub fn to_proto_string(&self) -> String {
428        let mut out = String::new();
429
430        // Comments
431        for comment in &self.comments {
432            out.push_str(&format!("// {}\n", comment));
433        }
434
435        out.push_str(&format!("service {} {{\n", self.name));
436
437        for method in &self.methods {
438            out.push_str(&format!("  {}\n", method.to_proto_string()));
439        }
440
441        out.push_str("}\n");
442        out
443    }
444}
445
446/// Represents an RPC method in a gRPC service.
447#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct ProtoRpcMethod {
449    /// Method name (e.g., "ProcessPayment")
450    pub name: String,
451    /// Request message type
452    pub request_type: String,
453    /// Response message type
454    pub response_type: String,
455    /// Streaming mode
456    pub streaming: StreamingMode,
457    /// Documentation comments
458    pub comments: Vec<String>,
459}
460
461impl ProtoRpcMethod {
462    /// Create a new unary RPC method.
463    pub fn new(
464        name: impl Into<String>,
465        request_type: impl Into<String>,
466        response_type: impl Into<String>,
467    ) -> Self {
468        Self {
469            name: name.into(),
470            request_type: request_type.into(),
471            response_type: response_type.into(),
472            streaming: StreamingMode::Unary,
473            comments: Vec::new(),
474        }
475    }
476
477    /// Serialize the method to `.proto` text format.
478    pub fn to_proto_string(&self) -> String {
479        let request = match self.streaming {
480            StreamingMode::ClientStreaming | StreamingMode::Bidirectional => {
481                format!("stream {}", self.request_type)
482            }
483            _ => self.request_type.clone(),
484        };
485
486        let response = match self.streaming {
487            StreamingMode::ServerStreaming | StreamingMode::Bidirectional => {
488                format!("stream {}", self.response_type)
489            }
490            _ => self.response_type.clone(),
491        };
492
493        format!("rpc {}({}) returns ({});", self.name, request, response)
494    }
495}
496
497/// gRPC streaming mode for RPC methods.
498#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
499pub enum StreamingMode {
500    /// Unary RPC (default): single request, single response
501    #[default]
502    Unary,
503    /// Server streaming: single request, stream of responses
504    ServerStreaming,
505    /// Client streaming: stream of requests, single response
506    ClientStreaming,
507    /// Bidirectional streaming: stream of requests, stream of responses
508    Bidirectional,
509}
510
511impl StreamingMode {
512    /// Parse streaming mode from a string (e.g., from Flow attributes).
513    pub fn parse(s: &str) -> Self {
514        match s.to_lowercase().as_str() {
515            "streaming" | "server_streaming" | "serverstreaming" => StreamingMode::ServerStreaming,
516            "client_streaming" | "clientstreaming" => StreamingMode::ClientStreaming,
517            "bidirectional" | "bidi" | "duplex" => StreamingMode::Bidirectional,
518            _ => StreamingMode::Unary,
519        }
520    }
521}
522
523impl std::fmt::Display for StreamingMode {
524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525        match self {
526            StreamingMode::Unary => write!(f, "unary"),
527            StreamingMode::ServerStreaming => write!(f, "server_streaming"),
528            StreamingMode::ClientStreaming => write!(f, "client_streaming"),
529            StreamingMode::Bidirectional => write!(f, "bidirectional"),
530        }
531    }
532}
533
534/// Represents a Protobuf message definition.
535#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct ProtoMessage {
537    /// Message name (PascalCase)
538    pub name: String,
539    /// Field definitions
540    pub fields: Vec<ProtoField>,
541    /// Nested message definitions
542    pub nested_messages: Vec<ProtoMessage>,
543    /// Nested enum definitions
544    pub nested_enums: Vec<ProtoEnum>,
545    /// Reserved field numbers
546    pub reserved_numbers: Vec<u32>,
547    /// Reserved field names
548    pub reserved_names: Vec<String>,
549    /// Documentation comments
550    pub comments: Vec<String>,
551}
552
553impl ProtoMessage {
554    /// Create a new empty ProtoMessage.
555    pub fn new(name: impl Into<String>) -> Self {
556        Self {
557            name: name.into(),
558            fields: Vec::new(),
559            nested_messages: Vec::new(),
560            nested_enums: Vec::new(),
561            reserved_numbers: Vec::new(),
562            reserved_names: Vec::new(),
563            comments: Vec::new(),
564        }
565    }
566
567    /// Serialize the message to `.proto` text format.
568    pub fn to_proto_string(&self) -> String {
569        let mut out = String::new();
570
571        // Comments
572        for comment in &self.comments {
573            out.push_str(&format!("// {}\n", comment));
574        }
575
576        out.push_str(&format!("message {} {{\n", self.name));
577
578        // Reserved fields
579        if !self.reserved_numbers.is_empty() {
580            let nums: Vec<String> = self
581                .reserved_numbers
582                .iter()
583                .map(|n| n.to_string())
584                .collect();
585            out.push_str(&format!("  reserved {};\n", nums.join(", ")));
586        }
587        if !self.reserved_names.is_empty() {
588            let names: Vec<String> = self
589                .reserved_names
590                .iter()
591                .map(|n| format!("\"{}\"", n))
592                .collect();
593            out.push_str(&format!("  reserved {};\n", names.join(", ")));
594        }
595
596        // Nested enums
597        for e in &self.nested_enums {
598            for line in e.to_proto_string().lines() {
599                out.push_str(&format!("  {}\n", line));
600            }
601        }
602
603        // Nested messages
604        for m in &self.nested_messages {
605            for line in m.to_proto_string().lines() {
606                out.push_str(&format!("  {}\n", line));
607            }
608        }
609
610        // Fields
611        for field in &self.fields {
612            out.push_str(&format!("  {}\n", field.to_proto_string()));
613        }
614
615        out.push_str("}\n");
616        out
617    }
618}
619
620/// Represents a Protobuf field definition.
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct ProtoField {
623    /// Field name (snake_case)
624    pub name: String,
625    /// Field number (must be unique within message)
626    pub number: u32,
627    /// Field type
628    pub proto_type: ProtoType,
629    /// Whether this is a repeated field
630    pub repeated: bool,
631    /// Whether this field is optional (proto3 optional)
632    pub optional: bool,
633    /// Documentation comments
634    pub comments: Vec<String>,
635}
636
637impl ProtoField {
638    /// Serialize the field to `.proto` text format.
639    pub fn to_proto_string(&self) -> String {
640        let mut parts = Vec::new();
641
642        // Comments as inline
643        if !self.comments.is_empty() {
644            // We'll add comment at the end
645        }
646
647        if self.optional {
648            parts.push("optional".to_string());
649        }
650        if self.repeated {
651            parts.push("repeated".to_string());
652        }
653
654        parts.push(self.proto_type.to_proto_string());
655        parts.push(self.name.clone());
656
657        let mut line = format!("{} = {};", parts.join(" "), self.number);
658
659        if !self.comments.is_empty() {
660            line.push_str(&format!(" // {}", self.comments.join("; ")));
661        }
662
663        line
664    }
665}
666
667/// Represents a Protobuf type reference.
668#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
669pub enum ProtoType {
670    /// Scalar types (int32, string, etc.)
671    Scalar(ScalarType),
672    /// Reference to another message type
673    Message(String),
674    /// Reference to an enum type
675    Enum(String),
676    /// Map type (map<key, value>)
677    Map {
678        key: Box<ProtoType>,
679        value: Box<ProtoType>,
680    },
681}
682
683impl ProtoType {
684    /// Serialize the type to `.proto` text format.
685    pub fn to_proto_string(&self) -> String {
686        match self {
687            ProtoType::Scalar(s) => s.to_proto_string(),
688            ProtoType::Message(name) => name.clone(),
689            ProtoType::Enum(name) => name.clone(),
690            ProtoType::Map { key, value } => {
691                format!(
692                    "map<{}, {}>",
693                    key.to_proto_string(),
694                    value.to_proto_string()
695                )
696            }
697        }
698    }
699}
700
701/// Protobuf scalar types.
702#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
703pub enum ScalarType {
704    Double,
705    Float,
706    Int32,
707    Int64,
708    Uint32,
709    Uint64,
710    Sint32,
711    Sint64,
712    Fixed32,
713    Fixed64,
714    Sfixed32,
715    Sfixed64,
716    Bool,
717    String,
718    Bytes,
719}
720
721impl ScalarType {
722    /// Serialize the scalar type to `.proto` text format.
723    pub fn to_proto_string(&self) -> String {
724        match self {
725            ScalarType::Double => "double",
726            ScalarType::Float => "float",
727            ScalarType::Int32 => "int32",
728            ScalarType::Int64 => "int64",
729            ScalarType::Uint32 => "uint32",
730            ScalarType::Uint64 => "uint64",
731            ScalarType::Sint32 => "sint32",
732            ScalarType::Sint64 => "sint64",
733            ScalarType::Fixed32 => "fixed32",
734            ScalarType::Fixed64 => "fixed64",
735            ScalarType::Sfixed32 => "sfixed32",
736            ScalarType::Sfixed64 => "sfixed64",
737            ScalarType::Bool => "bool",
738            ScalarType::String => "string",
739            ScalarType::Bytes => "bytes",
740        }
741        .to_string()
742    }
743}
744
745/// Represents a Protobuf enum definition.
746#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct ProtoEnum {
748    /// Enum name (PascalCase)
749    pub name: String,
750    /// Enum values
751    pub values: Vec<ProtoEnumValue>,
752    /// Documentation comments
753    pub comments: Vec<String>,
754}
755
756impl ProtoEnum {
757    /// Create a new ProtoEnum with default UNSPECIFIED value.
758    pub fn new(name: impl Into<String>) -> Self {
759        let name = name.into();
760        Self {
761            values: vec![ProtoEnumValue {
762                name: format!("{}_UNSPECIFIED", to_screaming_snake_case(&name)),
763                number: 0,
764            }],
765            name,
766            comments: Vec::new(),
767        }
768    }
769
770    /// Add a value to the enum.
771    pub fn add_value(&mut self, name: impl Into<String>) {
772        let number = self.values.len() as i32;
773        self.values.push(ProtoEnumValue {
774            name: name.into(),
775            number,
776        });
777    }
778
779    /// Serialize the enum to `.proto` text format.
780    pub fn to_proto_string(&self) -> String {
781        let mut out = String::new();
782
783        for comment in &self.comments {
784            out.push_str(&format!("// {}\n", comment));
785        }
786
787        out.push_str(&format!("enum {} {{\n", self.name));
788
789        for value in &self.values {
790            out.push_str(&format!("  {} = {};\n", value.name, value.number));
791        }
792
793        out.push_str("}\n");
794        out
795    }
796}
797
798/// Represents a Protobuf enum value.
799#[derive(Debug, Clone, Serialize, Deserialize)]
800pub struct ProtoEnumValue {
801    /// Value name (SCREAMING_SNAKE_CASE)
802    pub name: String,
803    /// Numeric value
804    pub number: i32,
805}
806
807// ============================================================================
808// Well-Known Types
809// ============================================================================
810
811/// Google Protobuf Well-Known Types.
812///
813/// These are standard types provided by Google that have special handling
814/// in most Protobuf implementations.
815#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
816pub enum WellKnownType {
817    /// google.protobuf.Timestamp - for date/time values
818    Timestamp,
819    /// google.protobuf.Duration - for time spans
820    Duration,
821    /// google.protobuf.Any - for dynamic typing
822    Any,
823    /// google.protobuf.Struct - for JSON-like structures
824    Struct,
825    /// google.protobuf.Value - for dynamic JSON values
826    Value,
827    /// google.protobuf.ListValue - for JSON arrays
828    ListValue,
829    /// google.protobuf.Empty - for empty messages
830    Empty,
831    /// Wrapper types for nullable primitives
832    Int32Value,
833    Int64Value,
834    UInt32Value,
835    UInt64Value,
836    FloatValue,
837    DoubleValue,
838    BoolValue,
839    StringValue,
840    BytesValue,
841}
842
843impl WellKnownType {
844    /// Get the fully qualified type name.
845    pub fn type_name(&self) -> &'static str {
846        match self {
847            WellKnownType::Timestamp => "google.protobuf.Timestamp",
848            WellKnownType::Duration => "google.protobuf.Duration",
849            WellKnownType::Any => "google.protobuf.Any",
850            WellKnownType::Struct => "google.protobuf.Struct",
851            WellKnownType::Value => "google.protobuf.Value",
852            WellKnownType::ListValue => "google.protobuf.ListValue",
853            WellKnownType::Empty => "google.protobuf.Empty",
854            WellKnownType::Int32Value => "google.protobuf.Int32Value",
855            WellKnownType::Int64Value => "google.protobuf.Int64Value",
856            WellKnownType::UInt32Value => "google.protobuf.UInt32Value",
857            WellKnownType::UInt64Value => "google.protobuf.UInt64Value",
858            WellKnownType::FloatValue => "google.protobuf.FloatValue",
859            WellKnownType::DoubleValue => "google.protobuf.DoubleValue",
860            WellKnownType::BoolValue => "google.protobuf.BoolValue",
861            WellKnownType::StringValue => "google.protobuf.StringValue",
862            WellKnownType::BytesValue => "google.protobuf.BytesValue",
863        }
864    }
865
866    /// Get the import path for this type.
867    pub fn import_path(&self) -> &'static str {
868        match self {
869            WellKnownType::Timestamp => "google/protobuf/timestamp.proto",
870            WellKnownType::Duration => "google/protobuf/duration.proto",
871            WellKnownType::Any => "google/protobuf/any.proto",
872            WellKnownType::Struct | WellKnownType::Value | WellKnownType::ListValue => {
873                "google/protobuf/struct.proto"
874            }
875            WellKnownType::Empty => "google/protobuf/empty.proto",
876            WellKnownType::Int32Value
877            | WellKnownType::Int64Value
878            | WellKnownType::UInt32Value
879            | WellKnownType::UInt64Value
880            | WellKnownType::FloatValue
881            | WellKnownType::DoubleValue
882            | WellKnownType::BoolValue
883            | WellKnownType::StringValue
884            | WellKnownType::BytesValue => "google/protobuf/wrappers.proto",
885        }
886    }
887
888    /// Try to parse a type name into a WellKnownType.
889    pub fn from_type_name(name: &str) -> Option<Self> {
890        match name {
891            "google.protobuf.Timestamp" => Some(WellKnownType::Timestamp),
892            "google.protobuf.Duration" => Some(WellKnownType::Duration),
893            "google.protobuf.Any" => Some(WellKnownType::Any),
894            "google.protobuf.Struct" => Some(WellKnownType::Struct),
895            "google.protobuf.Value" => Some(WellKnownType::Value),
896            "google.protobuf.ListValue" => Some(WellKnownType::ListValue),
897            "google.protobuf.Empty" => Some(WellKnownType::Empty),
898            "google.protobuf.Int32Value" => Some(WellKnownType::Int32Value),
899            "google.protobuf.Int64Value" => Some(WellKnownType::Int64Value),
900            "google.protobuf.UInt32Value" => Some(WellKnownType::UInt32Value),
901            "google.protobuf.UInt64Value" => Some(WellKnownType::UInt64Value),
902            "google.protobuf.FloatValue" => Some(WellKnownType::FloatValue),
903            "google.protobuf.DoubleValue" => Some(WellKnownType::DoubleValue),
904            "google.protobuf.BoolValue" => Some(WellKnownType::BoolValue),
905            "google.protobuf.StringValue" => Some(WellKnownType::StringValue),
906            "google.protobuf.BytesValue" => Some(WellKnownType::BytesValue),
907            _ => None,
908        }
909    }
910}
911
912// ============================================================================
913// Type Mapping
914// ============================================================================
915
916/// Map SEA type strings to Protobuf types.
917///
918/// This function handles the common type names used in SEA attributes,
919/// including mapping to Google Well-Known Types where appropriate.
920pub fn map_sea_type_to_proto(sea_type: &str) -> ProtoType {
921    match sea_type.to_lowercase().as_str() {
922        // Scalar types
923        "string" | "text" | "varchar" => ProtoType::Scalar(ScalarType::String),
924        "int" | "integer" | "int64" | "long" => ProtoType::Scalar(ScalarType::Int64),
925        "int32" | "short" => ProtoType::Scalar(ScalarType::Int32),
926        "uint32" => ProtoType::Scalar(ScalarType::Uint32),
927        "uint64" | "ulong" => ProtoType::Scalar(ScalarType::Uint64),
928        "float" | "double" | "decimal" | "number" => ProtoType::Scalar(ScalarType::Double),
929        "float32" => ProtoType::Scalar(ScalarType::Float),
930        "bool" | "boolean" => ProtoType::Scalar(ScalarType::Bool),
931        "bytes" | "binary" | "blob" => ProtoType::Scalar(ScalarType::Bytes),
932        "uuid" | "guid" => ProtoType::Scalar(ScalarType::String),
933
934        // Well-Known Types
935        "date" | "datetime" | "timestamp" => {
936            ProtoType::Message(WellKnownType::Timestamp.type_name().to_string())
937        }
938        "duration" | "timespan" | "interval" => {
939            ProtoType::Message(WellKnownType::Duration.type_name().to_string())
940        }
941        "any" | "dynamic" | "object" => {
942            ProtoType::Message(WellKnownType::Any.type_name().to_string())
943        }
944        "struct" | "json" | "jsonobject" => {
945            ProtoType::Message(WellKnownType::Struct.type_name().to_string())
946        }
947        "value" | "jsonvalue" => ProtoType::Message(WellKnownType::Value.type_name().to_string()),
948        "empty" | "void" | "unit" => {
949            ProtoType::Message(WellKnownType::Empty.type_name().to_string())
950        }
951
952        // Nullable wrapper types
953        "optional_int" | "nullable_int" | "int?" => {
954            ProtoType::Message(WellKnownType::Int64Value.type_name().to_string())
955        }
956        "optional_int32" | "nullable_int32" | "int32?" => {
957            ProtoType::Message(WellKnownType::Int32Value.type_name().to_string())
958        }
959        "optional_string" | "nullable_string" | "string?" => {
960            ProtoType::Message(WellKnownType::StringValue.type_name().to_string())
961        }
962        "optional_bool" | "nullable_bool" | "bool?" => {
963            ProtoType::Message(WellKnownType::BoolValue.type_name().to_string())
964        }
965        "optional_double" | "nullable_double" | "double?" => {
966            ProtoType::Message(WellKnownType::DoubleValue.type_name().to_string())
967        }
968        "optional_float" | "nullable_float" | "float?" => {
969            ProtoType::Message(WellKnownType::FloatValue.type_name().to_string())
970        }
971        "optional_bytes" | "nullable_bytes" | "bytes?" => {
972            ProtoType::Message(WellKnownType::BytesValue.type_name().to_string())
973        }
974
975        // Custom types become messages
976        _ => ProtoType::Message(to_pascal_case(sea_type)),
977    }
978}
979
980/// Infer a ProtoType from a serde_json::Value.
981pub fn infer_proto_type_from_value(value: &Value) -> ProtoType {
982    match value {
983        Value::Null => ProtoType::Scalar(ScalarType::String),
984        Value::Bool(_) => ProtoType::Scalar(ScalarType::Bool),
985        Value::Number(n) => {
986            if n.is_f64() {
987                ProtoType::Scalar(ScalarType::Double)
988            } else {
989                ProtoType::Scalar(ScalarType::Int64)
990            }
991        }
992        Value::String(_) => ProtoType::Scalar(ScalarType::String),
993        Value::Array(_) => ProtoType::Scalar(ScalarType::String), // Arrays need special handling
994        Value::Object(_) => ProtoType::Scalar(ScalarType::String), // Objects need special handling
995    }
996}
997
998// ============================================================================
999// Protobuf Engine
1000// ============================================================================
1001
1002/// The Protobuf projection engine.
1003///
1004/// This struct provides methods to convert a SEA Graph into Protobuf IR
1005/// which can then be serialized to `.proto` text format.
1006pub struct ProtobufEngine;
1007
1008impl ProtobufEngine {
1009    /// Project a SEA Graph to a ProtoFile.
1010    ///
1011    /// # Arguments
1012    ///
1013    /// * `graph` - The semantic graph to project
1014    /// * `namespace` - Filter to only include entities/resources from this namespace (empty = all)
1015    /// * `package` - The Protobuf package name to use
1016    ///
1017    /// # Returns
1018    ///
1019    /// A `ProtoFile` containing the generated Protobuf IR.
1020    pub fn project(graph: &Graph, namespace: &str, package: &str) -> ProtoFile {
1021        let mut proto = ProtoFile::new(package);
1022        proto.metadata.source_namespace = namespace.to_string();
1023        proto.metadata.generated_at = chrono::Utc::now().to_rfc3339();
1024
1025        // Convert entities to messages
1026        for entity in graph.all_entities() {
1027            if namespace.is_empty() || entity.namespace() == namespace {
1028                proto.messages.push(Self::entity_to_message(entity));
1029            }
1030        }
1031
1032        // Convert resources to messages
1033        for resource in graph.all_resources() {
1034            if namespace.is_empty() || resource.namespace() == namespace {
1035                proto.messages.push(Self::resource_to_message(resource));
1036            }
1037        }
1038
1039        // Sort messages by name for deterministic output
1040        proto.messages.sort_by(|a, b| a.name.cmp(&b.name));
1041
1042        // Auto-detect and add Well-Known Type imports
1043        proto.add_wkt_imports();
1044
1045        proto
1046    }
1047
1048    /// Project with options for a projection contract.
1049    pub fn project_with_options(
1050        graph: &Graph,
1051        namespace: &str,
1052        package: &str,
1053        projection_name: &str,
1054        include_governance: bool,
1055    ) -> ProtoFile {
1056        Self::project_with_full_options(
1057            graph,
1058            namespace,
1059            package,
1060            projection_name,
1061            include_governance,
1062            false, // include_services
1063        )
1064    }
1065
1066    /// Project with all options including gRPC service generation.
1067    pub fn project_with_full_options(
1068        graph: &Graph,
1069        namespace: &str,
1070        package: &str,
1071        projection_name: &str,
1072        include_governance: bool,
1073        include_services: bool,
1074    ) -> ProtoFile {
1075        let mut proto = Self::project(graph, namespace, package);
1076        proto.metadata.projection_name = projection_name.to_string();
1077
1078        if include_governance {
1079            proto.messages.extend(Self::generate_governance_messages());
1080        }
1081
1082        if include_services {
1083            proto.services = Self::flows_to_services(graph, namespace);
1084        }
1085
1086        // Re-add WKT imports in case governance messages need them
1087        proto.add_wkt_imports();
1088
1089        proto
1090    }
1091
1092    /// Project a SEA Graph to multiple ProtoFiles (one per namespace).
1093    ///
1094    /// This method partitions the graph by namespace, creating a separate `.proto` file
1095    /// for each. Cross-namespace references are automatically resolved by adding imports
1096    /// and fully qualifying type names.
1097    ///
1098    /// # Arguments
1099    ///
1100    /// * `graph` - The semantic graph to project
1101    /// * `base_package` - The root package name (e.g. "sea.generated")
1102    /// * `include_governance` - Whether to include governance messages
1103    /// * `include_services` - Whether to generate gRPC services
1104    ///
1105    /// # Returns
1106    ///
1107    /// A map of relative file paths to ProtoFile definitions.
1108    pub fn project_multi_file(
1109        graph: &Graph,
1110        base_package: &str,
1111        include_governance: bool,
1112        include_services: bool,
1113    ) -> BTreeMap<PathBuf, ProtoFile> {
1114        let mut files: BTreeMap<String, ProtoFile> = BTreeMap::new();
1115        // Index: TypeName -> (Namespace, FullPackage)
1116        let mut type_index: HashMap<String, (String, String)> = HashMap::new();
1117
1118        // 1. Collect all namespaces and entities
1119        for entity in graph.all_entities() {
1120            let ns = entity.namespace();
1121            let package = if ns.is_empty() {
1122                base_package.to_string()
1123            } else {
1124                format!("{}.{}", base_package, ns)
1125            };
1126
1127            type_index.insert(
1128                to_pascal_case(entity.name()),
1129                (ns.to_string(), package.clone()),
1130            );
1131
1132            files
1133                .entry(ns.to_string())
1134                .or_insert_with(|| ProtoFile::new(package))
1135                .messages
1136                .push(Self::entity_to_message(entity));
1137        }
1138
1139        for resource in graph.all_resources() {
1140            let ns = resource.namespace();
1141            let package = if ns.is_empty() {
1142                base_package.to_string()
1143            } else {
1144                format!("{}.{}", base_package, ns)
1145            };
1146
1147            type_index.insert(
1148                to_pascal_case(resource.name()),
1149                (ns.to_string(), package.clone()),
1150            );
1151
1152            files
1153                .entry(ns.to_string())
1154                .or_insert_with(|| ProtoFile::new(package))
1155                .messages
1156                .push(Self::resource_to_message(resource));
1157        }
1158
1159        // 2. Add Services
1160        if include_services {
1161            // Iterate all flows to capture services
1162            let services = Self::flows_to_services(graph, "");
1163            for service in services {
1164                // Try to find the namespace of the service based on the destination entity name
1165                // Service name is {DestEntity}Service
1166                let entity_name = service
1167                    .name
1168                    .strip_suffix("Service")
1169                    .unwrap_or(&service.name);
1170
1171                // Look up entity namespace in type_index
1172                if let Some((ns, package)) = type_index.get(entity_name) {
1173                    files
1174                        .entry(ns.clone())
1175                        .or_insert_with(|| ProtoFile::new(package.clone()))
1176                        .services
1177                        .push(service);
1178                } else {
1179                    // Default to base package if not found (e.g. flow to unknown entity)
1180                    let package = base_package.to_string();
1181                    files
1182                        .entry("".to_string())
1183                        .or_insert_with(|| ProtoFile::new(package))
1184                        .services
1185                        .push(service);
1186                }
1187            }
1188        }
1189
1190        // 3. Add Governance (in root namespace usually)
1191        if include_governance {
1192            let root_file = files
1193                .entry("".to_string())
1194                .or_insert_with(|| ProtoFile::new(base_package.to_string()));
1195
1196            let governance_msgs = Self::generate_governance_messages();
1197            for msg in &governance_msgs {
1198                type_index.insert(msg.name.clone(), ("".to_string(), base_package.to_string()));
1199            }
1200            root_file.messages.extend(governance_msgs);
1201        }
1202
1203        // 4. Resolve Imports and Finalize
1204        for (ns, file) in files.iter_mut() {
1205            let mut imports_to_add = HashSet::new();
1206
1207            // Check messages for cross-references
1208            for msg in &mut file.messages {
1209                Self::resolve_imports_in_message(msg, ns, &type_index, &mut imports_to_add);
1210            }
1211
1212            // Check services
1213            for svc in &mut file.services {
1214                for method in &mut svc.methods {
1215                    // Check request types
1216                    if let Some((target_ns, target_pkg)) = type_index.get(&method.request_type) {
1217                        if target_ns != ns {
1218                            imports_to_add.insert(target_ns.clone());
1219                            method.request_type = format!("{}.{}", target_pkg, method.request_type);
1220                        }
1221                    }
1222                    // Check response types
1223                    if let Some((target_ns, target_pkg)) = type_index.get(&method.response_type) {
1224                        if target_ns != ns {
1225                            imports_to_add.insert(target_ns.clone());
1226                            method.response_type =
1227                                format!("{}.{}", target_pkg, method.response_type);
1228                        }
1229                    }
1230                }
1231            }
1232
1233            // Add collected imports
1234            for target_ns in imports_to_add {
1235                // self-import is prevented by check above
1236                // root namespace usually doesn't need path prefix if files in same dir,
1237                // but assume we strictly follow directory structure.
1238                let import_path = if target_ns.is_empty() {
1239                    "projection.proto".to_string()
1240                } else {
1241                    format!("{}.proto", target_ns.replace('.', "/"))
1242                };
1243                file.imports.push(import_path);
1244            }
1245
1246            // Sort and deduplicate imports (including WKTs)
1247            file.add_wkt_imports();
1248        }
1249
1250        // Convert map to PathBuf keys
1251        let mut results = BTreeMap::new();
1252        for (ns, file) in files {
1253            let path = if ns.is_empty() {
1254                PathBuf::from("projection.proto")
1255            } else {
1256                PathBuf::from(format!("{}.proto", ns.replace('.', "/")))
1257            };
1258            results.insert(path, file);
1259        }
1260
1261        results
1262    }
1263
1264    /// Helper to resolve cross-namespace imports within a message.
1265    fn resolve_imports_in_message(
1266        msg: &mut ProtoMessage,
1267        current_ns: &str,
1268        index: &HashMap<String, (String, String)>,
1269        imports: &mut HashSet<String>,
1270    ) {
1271        for field in &mut msg.fields {
1272            match &mut field.proto_type {
1273                ProtoType::Message(name) | ProtoType::Enum(name) => {
1274                    // Ignore WKTs
1275                    if WellKnownType::from_type_name(name).is_some() {
1276                        continue;
1277                    }
1278
1279                    if let Some((target_ns, target_pkg)) = index.get(name.as_str()) {
1280                        if target_ns != current_ns {
1281                            imports.insert(target_ns.clone());
1282                            *name = format!("{}.{}", target_pkg, name);
1283                        }
1284                    }
1285                }
1286                ProtoType::Map { key: _, value } => {
1287                    // Check recursively (simplified for now assuming only value can be message)
1288                    if let ProtoType::Message(name) = &mut **value {
1289                        if WellKnownType::from_type_name(name).is_some() {
1290                            continue;
1291                        }
1292                        if let Some((target_ns, target_pkg)) = index.get(name.as_str()) {
1293                            if target_ns != current_ns {
1294                                imports.insert(target_ns.clone());
1295                                *name = format!("{}.{}", target_pkg, name);
1296                            }
1297                        }
1298                    }
1299                }
1300                _ => {}
1301            }
1302        }
1303
1304        // Recurse into nested messages
1305        for nested in &mut msg.nested_messages {
1306            Self::resolve_imports_in_message(nested, current_ns, index, imports);
1307        }
1308    }
1309    ///
1310    /// This method groups flows by their destination entity (service provider)
1311    /// and creates RPC methods for each flow. The naming convention follows
1312    /// gRPC best practices: `{DestinationEntity}Service`.
1313    ///
1314    /// # Example
1315    ///
1316    /// A flow `Customer -> PaymentProcessor of PaymentRequest` generates:
1317    /// ```protobuf
1318    /// service PaymentProcessorService {
1319    ///   rpc ProcessPaymentRequest(PaymentRequest) returns (PaymentRequestResponse);
1320    /// }
1321    /// ```
1322    pub fn flows_to_services(graph: &Graph, namespace: &str) -> Vec<ProtoService> {
1323        let mut services: BTreeMap<String, ProtoService> = BTreeMap::new();
1324
1325        for flow in graph.all_flows() {
1326            // Filter by namespace if specified
1327            if !namespace.is_empty() && flow.namespace() != namespace {
1328                continue;
1329            }
1330
1331            // Get the destination entity (service provider)
1332            let to_entity = match graph.get_entity(flow.to_id()) {
1333                Some(e) => e,
1334                None => continue, // Skip if entity not found
1335            };
1336
1337            // Get the resource being transferred (becomes the request type)
1338            let resource = match graph.get_resource(flow.resource_id()) {
1339                Some(r) => r,
1340                None => continue, // Skip if resource not found
1341            };
1342
1343            // Determine service name: {DestinationEntity}Service
1344            let service_name = format!("{}Service", to_pascal_case(to_entity.name()));
1345
1346            // Determine method name: Process{Resource} or Send{Resource}
1347            let method_name = format!("Process{}", to_pascal_case(resource.name()));
1348
1349            // Request type is the resource name
1350            let request_type = to_pascal_case(resource.name());
1351
1352            // Response type is {Resource}Response
1353            let response_type = format!("{}Response", to_pascal_case(resource.name()));
1354
1355            // Check for streaming mode in flow attributes
1356            let streaming = flow
1357                .get_attribute("streaming")
1358                .and_then(|v| v.as_str())
1359                .map(StreamingMode::parse)
1360                .unwrap_or(StreamingMode::Unary);
1361
1362            // Create the RPC method
1363            let mut method = ProtoRpcMethod::new(&method_name, &request_type, &response_type);
1364            method.streaming = streaming;
1365
1366            // Get the source entity for the comment
1367            if let Some(from_entity) = graph.get_entity(flow.from_id()) {
1368                method.comments.push(format!(
1369                    "Flow: {} -> {} of {}",
1370                    from_entity.name(),
1371                    to_entity.name(),
1372                    resource.name()
1373                ));
1374            }
1375
1376            // Add to or create service
1377            services
1378                .entry(service_name.clone())
1379                .or_insert_with(|| {
1380                    let mut svc = ProtoService::new(&service_name);
1381                    svc.comments
1382                        .push(format!("gRPC service for {}", to_entity.name()));
1383                    svc
1384                })
1385                .methods
1386                .push(method);
1387        }
1388
1389        // Also generate response messages for each service method
1390        // (This would normally be done, but we'll keep it simple for now)
1391
1392        services.into_values().collect()
1393    }
1394
1395    /// Convert an Entity to a ProtoMessage.
1396    fn entity_to_message(entity: &Entity) -> ProtoMessage {
1397        let mut msg = ProtoMessage::new(to_pascal_case(entity.name()));
1398        msg.comments.push(format!("SEA Entity: {}", entity.name()));
1399        msg.comments
1400            .push(format!("Namespace: {}", entity.namespace()));
1401
1402        let mut field_number = 1u32;
1403
1404        // Add id field (always first)
1405        msg.fields.push(ProtoField {
1406            name: "id".to_string(),
1407            number: field_number,
1408            proto_type: ProtoType::Scalar(ScalarType::String),
1409            repeated: false,
1410            optional: false,
1411            comments: vec!["Unique identifier".to_string()],
1412        });
1413        field_number += 1;
1414
1415        // Add name field
1416        msg.fields.push(ProtoField {
1417            name: "name".to_string(),
1418            number: field_number,
1419            proto_type: ProtoType::Scalar(ScalarType::String),
1420            repeated: false,
1421            optional: false,
1422            comments: vec!["Entity name".to_string()],
1423        });
1424        field_number += 1;
1425
1426        // Add attributes sorted alphabetically for deterministic output
1427        let mut sorted_attrs: BTreeMap<&String, &Value> = BTreeMap::new();
1428        for (key, value) in entity.attributes() {
1429            sorted_attrs.insert(key, value);
1430        }
1431
1432        for (key, value) in sorted_attrs {
1433            msg.fields.push(ProtoField {
1434                name: to_snake_case(key),
1435                number: field_number,
1436                proto_type: infer_proto_type_from_value(value),
1437                repeated: matches!(value, Value::Array(_)),
1438                optional: true,
1439                comments: vec![],
1440            });
1441            field_number += 1;
1442        }
1443
1444        msg
1445    }
1446
1447    /// Convert a Resource to a ProtoMessage.
1448    fn resource_to_message(resource: &Resource) -> ProtoMessage {
1449        let mut msg = ProtoMessage::new(to_pascal_case(resource.name()));
1450        msg.comments
1451            .push(format!("SEA Resource: {}", resource.name()));
1452        msg.comments
1453            .push(format!("Unit: {}", resource.unit().symbol()));
1454
1455        let mut field_number = 1u32;
1456
1457        // Add id field
1458        msg.fields.push(ProtoField {
1459            name: "id".to_string(),
1460            number: field_number,
1461            proto_type: ProtoType::Scalar(ScalarType::String),
1462            repeated: false,
1463            optional: false,
1464            comments: vec!["Unique identifier".to_string()],
1465        });
1466        field_number += 1;
1467
1468        // Add name field
1469        msg.fields.push(ProtoField {
1470            name: "name".to_string(),
1471            number: field_number,
1472            proto_type: ProtoType::Scalar(ScalarType::String),
1473            repeated: false,
1474            optional: false,
1475            comments: vec!["Resource name".to_string()],
1476        });
1477        field_number += 1;
1478
1479        // Add quantity field with unit comment
1480        msg.fields.push(ProtoField {
1481            name: "quantity".to_string(),
1482            number: field_number,
1483            proto_type: ProtoType::Scalar(ScalarType::Double),
1484            repeated: false,
1485            optional: true,
1486            comments: vec![format!("Quantity in {}", resource.unit().symbol())],
1487        });
1488        field_number += 1;
1489
1490        // Add unit field
1491        msg.fields.push(ProtoField {
1492            name: "unit".to_string(),
1493            number: field_number,
1494            proto_type: ProtoType::Scalar(ScalarType::String),
1495            repeated: false,
1496            optional: false,
1497            comments: vec!["Unit of measurement".to_string()],
1498        });
1499        field_number += 1;
1500
1501        // Add attributes
1502        let mut sorted_attrs: BTreeMap<&String, &Value> = BTreeMap::new();
1503        for (key, value) in resource.attributes() {
1504            sorted_attrs.insert(key, value);
1505        }
1506
1507        for (key, value) in sorted_attrs {
1508            msg.fields.push(ProtoField {
1509                name: to_snake_case(key),
1510                number: field_number,
1511                proto_type: infer_proto_type_from_value(value),
1512                repeated: matches!(value, Value::Array(_)),
1513                optional: true,
1514                comments: vec![],
1515            });
1516            field_number += 1;
1517        }
1518
1519        msg
1520    }
1521
1522    /// Generate standard governance messages.
1523    fn generate_governance_messages() -> Vec<ProtoMessage> {
1524        let mut messages = Vec::new();
1525
1526        // PolicyViolation message
1527        let mut violation = ProtoMessage::new("PolicyViolation");
1528        violation
1529            .comments
1530            .push("Represents a policy violation event".to_string());
1531        violation.fields = vec![
1532            ProtoField {
1533                name: "policy_name".to_string(),
1534                number: 1,
1535                proto_type: ProtoType::Scalar(ScalarType::String),
1536                repeated: false,
1537                optional: false,
1538                comments: vec!["Name of the violated policy".to_string()],
1539            },
1540            ProtoField {
1541                name: "entity_id".to_string(),
1542                number: 2,
1543                proto_type: ProtoType::Scalar(ScalarType::String),
1544                repeated: false,
1545                optional: false,
1546                comments: vec!["ID of the entity that violated the policy".to_string()],
1547            },
1548            ProtoField {
1549                name: "severity".to_string(),
1550                number: 3,
1551                proto_type: ProtoType::Scalar(ScalarType::String),
1552                repeated: false,
1553                optional: false,
1554                comments: vec!["Severity level (error, warn, info)".to_string()],
1555            },
1556            ProtoField {
1557                name: "message".to_string(),
1558                number: 4,
1559                proto_type: ProtoType::Scalar(ScalarType::String),
1560                repeated: false,
1561                optional: false,
1562                comments: vec!["Human-readable violation message".to_string()],
1563            },
1564            ProtoField {
1565                name: "timestamp".to_string(),
1566                number: 5,
1567                proto_type: ProtoType::Scalar(ScalarType::String),
1568                repeated: false,
1569                optional: false,
1570                comments: vec!["When the violation occurred".to_string()],
1571            },
1572        ];
1573        messages.push(violation);
1574
1575        // MetricEvent message
1576        let mut metric = ProtoMessage::new("MetricEvent");
1577        metric
1578            .comments
1579            .push("Represents a metric measurement event".to_string());
1580        metric.fields = vec![
1581            ProtoField {
1582                name: "metric_name".to_string(),
1583                number: 1,
1584                proto_type: ProtoType::Scalar(ScalarType::String),
1585                repeated: false,
1586                optional: false,
1587                comments: vec!["Name of the metric".to_string()],
1588            },
1589            ProtoField {
1590                name: "value".to_string(),
1591                number: 2,
1592                proto_type: ProtoType::Scalar(ScalarType::Double),
1593                repeated: false,
1594                optional: false,
1595                comments: vec!["Measured value".to_string()],
1596            },
1597            ProtoField {
1598                name: "unit".to_string(),
1599                number: 3,
1600                proto_type: ProtoType::Scalar(ScalarType::String),
1601                repeated: false,
1602                optional: true,
1603                comments: vec!["Unit of measurement".to_string()],
1604            },
1605            ProtoField {
1606                name: "timestamp".to_string(),
1607                number: 4,
1608                proto_type: ProtoType::Scalar(ScalarType::String),
1609                repeated: false,
1610                optional: false,
1611                comments: vec!["When the measurement was taken".to_string()],
1612            },
1613        ];
1614        messages.push(metric);
1615
1616        messages
1617    }
1618}
1619
1620// ============================================================================
1621// Helper Functions
1622// ============================================================================
1623
1624/// Convert a string to PascalCase.
1625fn to_pascal_case(s: &str) -> String {
1626    s.split(|c: char| !c.is_alphanumeric())
1627        .filter(|part| !part.is_empty())
1628        .map(|part| {
1629            let mut chars = part.chars();
1630            match chars.next() {
1631                Some(first) => first.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
1632                None => String::new(),
1633            }
1634        })
1635        .collect()
1636}
1637
1638/// Convert a string to snake_case.
1639fn to_snake_case(s: &str) -> String {
1640    let mut result = String::new();
1641    let mut prev_is_uppercase = false;
1642
1643    for (i, c) in s.chars().enumerate() {
1644        if c.is_uppercase() {
1645            if i > 0 && !prev_is_uppercase {
1646                result.push('_');
1647            }
1648            result.push(c.to_lowercase().next().unwrap_or(c));
1649            prev_is_uppercase = true;
1650        } else if c.is_alphanumeric() {
1651            result.push(c);
1652            prev_is_uppercase = false;
1653        } else {
1654            if !result.is_empty() && !result.ends_with('_') {
1655                result.push('_');
1656            }
1657            prev_is_uppercase = false;
1658        }
1659    }
1660
1661    result.trim_matches('_').to_string()
1662}
1663
1664/// Convert a string to SCREAMING_SNAKE_CASE.
1665fn to_screaming_snake_case(s: &str) -> String {
1666    to_snake_case(s).to_uppercase()
1667}
1668
1669// ============================================================================
1670// Compatibility Enforcement
1671// ============================================================================
1672
1673/// Compatibility mode for schema evolution.
1674#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1675pub enum CompatibilityMode {
1676    /// Only additions allowed - strictest mode for public APIs
1677    Additive,
1678    /// Removals become reserved fields - default for internal APIs
1679    #[default]
1680    Backward,
1681    /// All changes allowed - for breaking releases
1682    Breaking,
1683}
1684
1685impl std::fmt::Display for CompatibilityMode {
1686    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1687        match self {
1688            CompatibilityMode::Additive => write!(f, "additive"),
1689            CompatibilityMode::Backward => write!(f, "backward"),
1690            CompatibilityMode::Breaking => write!(f, "breaking"),
1691        }
1692    }
1693}
1694
1695impl std::str::FromStr for CompatibilityMode {
1696    type Err = String;
1697
1698    fn from_str(s: &str) -> Result<Self, Self::Err> {
1699        match s.to_lowercase().as_str() {
1700            "additive" | "strict" => Ok(CompatibilityMode::Additive),
1701            "backward" | "backwards" | "default" => Ok(CompatibilityMode::Backward),
1702            "breaking" | "none" => Ok(CompatibilityMode::Breaking),
1703            _ => Err(format!("Unknown compatibility mode: {}", s)),
1704        }
1705    }
1706}
1707
1708/// A compatibility violation found during schema comparison.
1709#[derive(Debug, Clone, Serialize, Deserialize)]
1710pub struct CompatibilityViolation {
1711    /// The message name where the violation occurred
1712    pub message_name: String,
1713    /// The field name involved (if applicable)
1714    pub field_name: Option<String>,
1715    /// The field number involved (if applicable)
1716    pub field_number: Option<u32>,
1717    /// Type of violation
1718    pub violation_type: ViolationType,
1719    /// Human-readable description
1720    pub description: String,
1721}
1722
1723/// Types of compatibility violations.
1724#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1725pub enum ViolationType {
1726    /// A field was removed
1727    FieldRemoved,
1728    /// A field number was reused with a different name/type
1729    FieldNumberReused,
1730    /// A field type was changed
1731    FieldTypeChanged,
1732    /// A field was renamed (same number, different name)
1733    FieldRenamed,
1734    /// A message was removed
1735    MessageRemoved,
1736    /// A required field was added (breaking in proto3)
1737    RequiredFieldAdded,
1738}
1739
1740impl std::fmt::Display for ViolationType {
1741    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1742        match self {
1743            ViolationType::FieldRemoved => write!(f, "field_removed"),
1744            ViolationType::FieldNumberReused => write!(f, "field_number_reused"),
1745            ViolationType::FieldTypeChanged => write!(f, "field_type_changed"),
1746            ViolationType::FieldRenamed => write!(f, "field_renamed"),
1747            ViolationType::MessageRemoved => write!(f, "message_removed"),
1748            ViolationType::RequiredFieldAdded => write!(f, "required_field_added"),
1749        }
1750    }
1751}
1752
1753/// Result of a compatibility check.
1754#[derive(Debug, Clone, Serialize, Deserialize)]
1755pub struct CompatibilityResult {
1756    /// Whether the schemas are compatible under the given mode
1757    pub is_compatible: bool,
1758    /// The mode used for checking
1759    pub mode: CompatibilityMode,
1760    /// List of violations found
1761    pub violations: Vec<CompatibilityViolation>,
1762    /// Suggested fixes (reserved fields to add)
1763    pub suggested_reserved_numbers: BTreeMap<String, Vec<u32>>,
1764    /// Suggested reserved names
1765    pub suggested_reserved_names: BTreeMap<String, Vec<String>>,
1766}
1767
1768impl CompatibilityResult {
1769    /// Create a compatible (empty) result.
1770    pub fn compatible(mode: CompatibilityMode) -> Self {
1771        Self {
1772            is_compatible: true,
1773            mode,
1774            violations: Vec::new(),
1775            suggested_reserved_numbers: BTreeMap::new(),
1776            suggested_reserved_names: BTreeMap::new(),
1777        }
1778    }
1779
1780    /// Check if there are any violations.
1781    pub fn has_violations(&self) -> bool {
1782        !self.violations.is_empty()
1783    }
1784
1785    /// Format violations as a human-readable report.
1786    pub fn to_report(&self) -> String {
1787        let mut out = String::new();
1788        out.push_str(&format!("Compatibility Check (mode: {})\n", self.mode));
1789        out.push_str(&format!(
1790            "Result: {}\n",
1791            if self.is_compatible { "PASS" } else { "FAIL" }
1792        ));
1793
1794        if !self.violations.is_empty() {
1795            out.push_str(&format!("\nViolations ({}):\n", self.violations.len()));
1796            for v in &self.violations {
1797                out.push_str(&format!(
1798                    "  - [{}] {}: {}\n",
1799                    v.violation_type, v.message_name, v.description
1800                ));
1801            }
1802        }
1803
1804        if !self.suggested_reserved_numbers.is_empty() {
1805            out.push_str("\nSuggested Reserved Fields:\n");
1806            for (msg, nums) in &self.suggested_reserved_numbers {
1807                let nums_str: Vec<String> = nums.iter().map(|n| n.to_string()).collect();
1808                out.push_str(&format!(
1809                    "  message {}: reserved {};\n",
1810                    msg,
1811                    nums_str.join(", ")
1812                ));
1813            }
1814        }
1815
1816        out
1817    }
1818}
1819
1820/// Schema compatibility checker.
1821pub struct CompatibilityChecker;
1822
1823impl CompatibilityChecker {
1824    /// Check compatibility between an old and new ProtoFile.
1825    ///
1826    /// # Arguments
1827    /// * `old` - The previous schema version
1828    /// * `new` - The new schema version  
1829    /// * `mode` - The compatibility mode to enforce
1830    ///
1831    /// # Returns
1832    /// A CompatibilityResult with violations and suggested fixes.
1833    pub fn check(old: &ProtoFile, new: &ProtoFile, mode: CompatibilityMode) -> CompatibilityResult {
1834        let mut result = CompatibilityResult::compatible(mode);
1835
1836        // Build lookup maps for old schema
1837        let old_messages: BTreeMap<&str, &ProtoMessage> =
1838            old.messages.iter().map(|m| (m.name.as_str(), m)).collect();
1839
1840        let new_messages: BTreeMap<&str, &ProtoMessage> =
1841            new.messages.iter().map(|m| (m.name.as_str(), m)).collect();
1842
1843        // Check for removed messages
1844        for name in old_messages.keys() {
1845            if !new_messages.contains_key(name) {
1846                result.violations.push(CompatibilityViolation {
1847                    message_name: name.to_string(),
1848                    field_name: None,
1849                    field_number: None,
1850                    violation_type: ViolationType::MessageRemoved,
1851                    description: format!("Message '{}' was removed", name),
1852                });
1853            }
1854        }
1855
1856        // Check each message that exists in both
1857        for (name, old_msg) in &old_messages {
1858            if let Some(new_msg) = new_messages.get(name) {
1859                Self::check_message(old_msg, new_msg, &mut result);
1860            }
1861        }
1862
1863        // Determine if compatible based on mode
1864        result.is_compatible = match mode {
1865            CompatibilityMode::Breaking => true, // Always compatible in breaking mode
1866            CompatibilityMode::Backward => {
1867                // Compatible if no field number reuse or type changes
1868                !result.violations.iter().any(|v| {
1869                    matches!(
1870                        v.violation_type,
1871                        ViolationType::FieldNumberReused | ViolationType::FieldTypeChanged
1872                    )
1873                })
1874            }
1875            CompatibilityMode::Additive => {
1876                // Any removal or change is incompatible
1877                result.violations.is_empty()
1878            }
1879        };
1880
1881        result
1882    }
1883
1884    /// Check compatibility between two messages.
1885    fn check_message(old: &ProtoMessage, new: &ProtoMessage, result: &mut CompatibilityResult) {
1886        // Build field maps by number and by name
1887        let old_by_number: BTreeMap<u32, &ProtoField> =
1888            old.fields.iter().map(|f| (f.number, f)).collect();
1889
1890        let new_by_number: BTreeMap<u32, &ProtoField> =
1891            new.fields.iter().map(|f| (f.number, f)).collect();
1892
1893        let old_by_name: BTreeMap<&str, &ProtoField> =
1894            old.fields.iter().map(|f| (f.name.as_str(), f)).collect();
1895
1896        // Check for removed fields
1897        for (number, old_field) in &old_by_number {
1898            if !new_by_number.contains_key(number) {
1899                result.violations.push(CompatibilityViolation {
1900                    message_name: old.name.clone(),
1901                    field_name: Some(old_field.name.clone()),
1902                    field_number: Some(*number),
1903                    violation_type: ViolationType::FieldRemoved,
1904                    description: format!(
1905                        "Field '{}' (number {}) was removed",
1906                        old_field.name, number
1907                    ),
1908                });
1909
1910                // Suggest reserving this field number
1911                result
1912                    .suggested_reserved_numbers
1913                    .entry(old.name.clone())
1914                    .or_default()
1915                    .push(*number);
1916
1917                result
1918                    .suggested_reserved_names
1919                    .entry(old.name.clone())
1920                    .or_default()
1921                    .push(old_field.name.clone());
1922            }
1923        }
1924
1925        // Check for field number reuse with different name/type
1926        for (number, new_field) in &new_by_number {
1927            if let Some(old_field) = old_by_number.get(number) {
1928                // Check name change
1929                if old_field.name != new_field.name {
1930                    result.violations.push(CompatibilityViolation {
1931                        message_name: old.name.clone(),
1932                        field_name: Some(new_field.name.clone()),
1933                        field_number: Some(*number),
1934                        violation_type: ViolationType::FieldRenamed,
1935                        description: format!(
1936                            "Field number {} renamed from '{}' to '{}'",
1937                            number, old_field.name, new_field.name
1938                        ),
1939                    });
1940                }
1941
1942                // Check type change
1943                if old_field.proto_type != new_field.proto_type {
1944                    result.violations.push(CompatibilityViolation {
1945                        message_name: old.name.clone(),
1946                        field_name: Some(new_field.name.clone()),
1947                        field_number: Some(*number),
1948                        violation_type: ViolationType::FieldTypeChanged,
1949                        description: format!(
1950                            "Field '{}' type changed from {} to {}",
1951                            new_field.name,
1952                            old_field.proto_type.to_proto_string(),
1953                            new_field.proto_type.to_proto_string()
1954                        ),
1955                    });
1956                }
1957            } else {
1958                // New field - check if it reuses a previously removed name
1959                if old_by_name.contains_key(new_field.name.as_str()) {
1960                    let old_field = old_by_name[new_field.name.as_str()];
1961                    if old_field.number != *number {
1962                        result.violations.push(CompatibilityViolation {
1963                            message_name: old.name.clone(),
1964                            field_name: Some(new_field.name.clone()),
1965                            field_number: Some(*number),
1966                            violation_type: ViolationType::FieldNumberReused,
1967                            description: format!(
1968                                "Field '{}' changed number from {} to {}",
1969                                new_field.name, old_field.number, number
1970                            ),
1971                        });
1972                    }
1973                }
1974            }
1975        }
1976    }
1977
1978    /// Apply compatibility fixes to a new ProtoFile based on an old one.
1979    ///
1980    /// This adds reserved field numbers and names for removed fields.
1981    pub fn apply_backward_compatibility(old: &ProtoFile, new: &mut ProtoFile) {
1982        let result = Self::check(old, new, CompatibilityMode::Backward);
1983
1984        // Apply suggested reserved numbers
1985        for msg in &mut new.messages {
1986            if let Some(reserved_nums) = result.suggested_reserved_numbers.get(&msg.name) {
1987                for num in reserved_nums {
1988                    if !msg.reserved_numbers.contains(num) {
1989                        msg.reserved_numbers.push(*num);
1990                    }
1991                }
1992                msg.reserved_numbers.sort();
1993            }
1994
1995            if let Some(reserved_names) = result.suggested_reserved_names.get(&msg.name) {
1996                for name in reserved_names {
1997                    if !msg.reserved_names.contains(name) {
1998                        msg.reserved_names.push(name.clone());
1999                    }
2000                }
2001                msg.reserved_names.sort();
2002            }
2003        }
2004    }
2005}
2006
2007/// File-based schema history storage.
2008pub struct SchemaHistory {
2009    /// Directory where schema history is stored
2010    history_dir: std::path::PathBuf,
2011}
2012
2013impl SchemaHistory {
2014    /// Create a new SchemaHistory with the given directory.
2015    pub fn new(history_dir: impl Into<std::path::PathBuf>) -> Self {
2016        Self {
2017            history_dir: history_dir.into(),
2018        }
2019    }
2020
2021    /// Get the path for a schema file.
2022    fn schema_path(&self, package: &str) -> std::path::PathBuf {
2023        let filename = format!("{}.json", package.replace('.', "_"));
2024        self.history_dir.join(filename)
2025    }
2026
2027    /// Load the previous schema for a package.
2028    pub fn load(&self, package: &str) -> Result<Option<ProtoFile>, String> {
2029        let path = self.schema_path(package);
2030        if !path.exists() {
2031            return Ok(None);
2032        }
2033
2034        let content = std::fs::read_to_string(&path)
2035            .map_err(|e| format!("Failed to read schema history: {}", e))?;
2036
2037        let proto: ProtoFile = serde_json::from_str(&content)
2038            .map_err(|e| format!("Failed to parse schema history: {}", e))?;
2039
2040        Ok(Some(proto))
2041    }
2042
2043    /// Save a schema to history.
2044    pub fn save(&self, proto: &ProtoFile) -> Result<(), String> {
2045        // Ensure directory exists
2046        std::fs::create_dir_all(&self.history_dir)
2047            .map_err(|e| format!("Failed to create history directory: {}", e))?;
2048
2049        let path = self.schema_path(&proto.package);
2050        let content = serde_json::to_string_pretty(proto)
2051            .map_err(|e| format!("Failed to serialize schema: {}", e))?;
2052
2053        std::fs::write(&path, content)
2054            .map_err(|e| format!("Failed to write schema history: {}", e))?;
2055
2056        Ok(())
2057    }
2058
2059    /// Check compatibility and optionally apply fixes.
2060    pub fn check_and_update(
2061        &self,
2062        new: &mut ProtoFile,
2063        mode: CompatibilityMode,
2064        apply_fixes: bool,
2065    ) -> Result<CompatibilityResult, String> {
2066        let old = self.load(&new.package)?;
2067
2068        let result = match old {
2069            Some(ref old_proto) => {
2070                if apply_fixes && mode == CompatibilityMode::Backward {
2071                    CompatibilityChecker::apply_backward_compatibility(old_proto, new);
2072                }
2073                CompatibilityChecker::check(old_proto, new, mode)
2074            }
2075            None => CompatibilityResult::compatible(mode),
2076        };
2077
2078        // Save the new schema if compatible (or in breaking mode)
2079        if result.is_compatible || mode == CompatibilityMode::Breaking {
2080            self.save(new)?;
2081        }
2082
2083        Ok(result)
2084    }
2085}
2086
2087// ============================================================================
2088// Tests
2089// ============================================================================
2090
2091#[cfg(test)]
2092mod tests {
2093    use super::*;
2094
2095    #[test]
2096    fn test_to_pascal_case() {
2097        assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
2098        assert_eq!(to_pascal_case("my-entity"), "MyEntity");
2099        assert_eq!(to_pascal_case("already PascalCase"), "AlreadyPascalcase");
2100        assert_eq!(to_pascal_case("UPPERCASE"), "Uppercase");
2101    }
2102
2103    #[test]
2104    fn test_to_snake_case() {
2105        assert_eq!(to_snake_case("HelloWorld"), "hello_world");
2106        assert_eq!(to_snake_case("myEntity"), "my_entity");
2107        assert_eq!(to_snake_case("already_snake"), "already_snake");
2108        // Consecutive uppercase chars become lowercase without underscore separation
2109        assert_eq!(to_snake_case("XMLParser"), "xmlparser");
2110        assert_eq!(to_snake_case("PaymentID"), "payment_id");
2111    }
2112
2113    #[test]
2114    fn test_to_screaming_snake_case() {
2115        assert_eq!(to_screaming_snake_case("MyEnum"), "MY_ENUM");
2116        assert_eq!(to_screaming_snake_case("StatusCode"), "STATUS_CODE");
2117    }
2118
2119    #[test]
2120    fn test_map_sea_type_to_proto() {
2121        assert_eq!(
2122            map_sea_type_to_proto("string"),
2123            ProtoType::Scalar(ScalarType::String)
2124        );
2125        assert_eq!(
2126            map_sea_type_to_proto("int"),
2127            ProtoType::Scalar(ScalarType::Int64)
2128        );
2129        assert_eq!(
2130            map_sea_type_to_proto("boolean"),
2131            ProtoType::Scalar(ScalarType::Bool)
2132        );
2133        assert_eq!(
2134            map_sea_type_to_proto("timestamp"),
2135            ProtoType::Message("google.protobuf.Timestamp".to_string())
2136        );
2137        assert_eq!(
2138            map_sea_type_to_proto("CustomType"),
2139            ProtoType::Message("Customtype".to_string())
2140        );
2141    }
2142
2143    #[test]
2144    fn test_proto_field_to_string() {
2145        let field = ProtoField {
2146            name: "my_field".to_string(),
2147            number: 1,
2148            proto_type: ProtoType::Scalar(ScalarType::String),
2149            repeated: false,
2150            optional: false,
2151            comments: vec![],
2152        };
2153        assert_eq!(field.to_proto_string(), "string my_field = 1;");
2154
2155        let optional_field = ProtoField {
2156            name: "optional_field".to_string(),
2157            number: 2,
2158            proto_type: ProtoType::Scalar(ScalarType::Int64),
2159            repeated: false,
2160            optional: true,
2161            comments: vec![],
2162        };
2163        assert_eq!(
2164            optional_field.to_proto_string(),
2165            "optional int64 optional_field = 2;"
2166        );
2167
2168        let repeated_field = ProtoField {
2169            name: "items".to_string(),
2170            number: 3,
2171            proto_type: ProtoType::Scalar(ScalarType::String),
2172            repeated: true,
2173            optional: false,
2174            comments: vec![],
2175        };
2176        assert_eq!(
2177            repeated_field.to_proto_string(),
2178            "repeated string items = 3;"
2179        );
2180    }
2181
2182    #[test]
2183    fn test_proto_enum_to_string() {
2184        let mut e = ProtoEnum::new("Status");
2185        e.add_value("STATUS_ACTIVE");
2186        e.add_value("STATUS_INACTIVE");
2187
2188        let output = e.to_proto_string();
2189        assert!(output.contains("enum Status {"));
2190        assert!(output.contains("STATUS_UNSPECIFIED = 0;"));
2191        assert!(output.contains("STATUS_ACTIVE = 1;"));
2192        assert!(output.contains("STATUS_INACTIVE = 2;"));
2193    }
2194
2195    #[test]
2196    fn test_proto_message_to_string() {
2197        let mut msg = ProtoMessage::new("Person");
2198        msg.fields.push(ProtoField {
2199            name: "name".to_string(),
2200            number: 1,
2201            proto_type: ProtoType::Scalar(ScalarType::String),
2202            repeated: false,
2203            optional: false,
2204            comments: vec![],
2205        });
2206        msg.fields.push(ProtoField {
2207            name: "age".to_string(),
2208            number: 2,
2209            proto_type: ProtoType::Scalar(ScalarType::Int32),
2210            repeated: false,
2211            optional: true,
2212            comments: vec![],
2213        });
2214
2215        let output = msg.to_proto_string();
2216        assert!(output.contains("message Person {"));
2217        assert!(output.contains("string name = 1;"));
2218        assert!(output.contains("optional int32 age = 2;"));
2219    }
2220
2221    #[test]
2222    fn test_proto_file_to_string() {
2223        let mut proto = ProtoFile::new("test.package");
2224        proto.metadata.projection_name = "TestProjection".to_string();
2225        proto.metadata.source_namespace = "test".to_string();
2226
2227        let mut msg = ProtoMessage::new("TestMessage");
2228        msg.fields.push(ProtoField {
2229            name: "id".to_string(),
2230            number: 1,
2231            proto_type: ProtoType::Scalar(ScalarType::String),
2232            repeated: false,
2233            optional: false,
2234            comments: vec![],
2235        });
2236        proto.messages.push(msg);
2237
2238        let output = proto.to_proto_string();
2239        assert!(output.contains("syntax = \"proto3\";"));
2240        assert!(output.contains("package test.package;"));
2241        assert!(output.contains("message TestMessage {"));
2242        assert!(output.contains("string id = 1;"));
2243    }
2244
2245    #[test]
2246    fn test_entity_to_message() {
2247        use serde_json::json;
2248
2249        let mut entity = Entity::new_with_namespace("Warehouse", "logistics");
2250        entity.set_attribute("capacity", json!(5000));
2251        entity.set_attribute("location", json!("Building A"));
2252
2253        let msg = ProtobufEngine::entity_to_message(&entity);
2254
2255        assert_eq!(msg.name, "Warehouse");
2256        assert!(msg.fields.iter().any(|f| f.name == "id"));
2257        assert!(msg.fields.iter().any(|f| f.name == "name"));
2258        assert!(msg.fields.iter().any(|f| f.name == "capacity"));
2259        assert!(msg.fields.iter().any(|f| f.name == "location"));
2260
2261        // Check field numbers are sequential
2262        let numbers: Vec<u32> = msg.fields.iter().map(|f| f.number).collect();
2263        assert_eq!(numbers, vec![1, 2, 3, 4]); // id, name, capacity, location (sorted)
2264    }
2265
2266    #[test]
2267    fn test_governance_messages() {
2268        let messages = ProtobufEngine::generate_governance_messages();
2269        assert_eq!(messages.len(), 2);
2270
2271        let violation = messages.iter().find(|m| m.name == "PolicyViolation");
2272        assert!(violation.is_some());
2273
2274        let metric = messages.iter().find(|m| m.name == "MetricEvent");
2275        assert!(metric.is_some());
2276    }
2277
2278    // ========================================================================
2279    // Well-Known Type Tests
2280    // ========================================================================
2281
2282    #[test]
2283    fn test_wkt_type_name() {
2284        assert_eq!(
2285            WellKnownType::Timestamp.type_name(),
2286            "google.protobuf.Timestamp"
2287        );
2288        assert_eq!(
2289            WellKnownType::Duration.type_name(),
2290            "google.protobuf.Duration"
2291        );
2292        assert_eq!(WellKnownType::Any.type_name(), "google.protobuf.Any");
2293        assert_eq!(WellKnownType::Struct.type_name(), "google.protobuf.Struct");
2294        assert_eq!(WellKnownType::Empty.type_name(), "google.protobuf.Empty");
2295        assert_eq!(
2296            WellKnownType::Int64Value.type_name(),
2297            "google.protobuf.Int64Value"
2298        );
2299    }
2300
2301    #[test]
2302    fn test_wkt_import_path() {
2303        assert_eq!(
2304            WellKnownType::Timestamp.import_path(),
2305            "google/protobuf/timestamp.proto"
2306        );
2307        assert_eq!(
2308            WellKnownType::Duration.import_path(),
2309            "google/protobuf/duration.proto"
2310        );
2311        assert_eq!(
2312            WellKnownType::Any.import_path(),
2313            "google/protobuf/any.proto"
2314        );
2315        assert_eq!(
2316            WellKnownType::Struct.import_path(),
2317            "google/protobuf/struct.proto"
2318        );
2319        assert_eq!(
2320            WellKnownType::Value.import_path(),
2321            "google/protobuf/struct.proto"
2322        );
2323        assert_eq!(
2324            WellKnownType::Empty.import_path(),
2325            "google/protobuf/empty.proto"
2326        );
2327        assert_eq!(
2328            WellKnownType::Int64Value.import_path(),
2329            "google/protobuf/wrappers.proto"
2330        );
2331        assert_eq!(
2332            WellKnownType::StringValue.import_path(),
2333            "google/protobuf/wrappers.proto"
2334        );
2335    }
2336
2337    #[test]
2338    fn test_wkt_from_type_name() {
2339        assert_eq!(
2340            WellKnownType::from_type_name("google.protobuf.Timestamp"),
2341            Some(WellKnownType::Timestamp)
2342        );
2343        assert_eq!(
2344            WellKnownType::from_type_name("google.protobuf.Duration"),
2345            Some(WellKnownType::Duration)
2346        );
2347        assert_eq!(
2348            WellKnownType::from_type_name("google.protobuf.Any"),
2349            Some(WellKnownType::Any)
2350        );
2351        assert_eq!(
2352            WellKnownType::from_type_name("google.protobuf.StringValue"),
2353            Some(WellKnownType::StringValue)
2354        );
2355        assert_eq!(WellKnownType::from_type_name("SomeOtherType"), None);
2356    }
2357
2358    #[test]
2359    fn test_map_sea_type_to_wkt() {
2360        // Timestamp types
2361        assert_eq!(
2362            map_sea_type_to_proto("timestamp"),
2363            ProtoType::Message("google.protobuf.Timestamp".to_string())
2364        );
2365        assert_eq!(
2366            map_sea_type_to_proto("datetime"),
2367            ProtoType::Message("google.protobuf.Timestamp".to_string())
2368        );
2369
2370        // Duration types
2371        assert_eq!(
2372            map_sea_type_to_proto("duration"),
2373            ProtoType::Message("google.protobuf.Duration".to_string())
2374        );
2375        assert_eq!(
2376            map_sea_type_to_proto("timespan"),
2377            ProtoType::Message("google.protobuf.Duration".to_string())
2378        );
2379
2380        // Dynamic types
2381        assert_eq!(
2382            map_sea_type_to_proto("any"),
2383            ProtoType::Message("google.protobuf.Any".to_string())
2384        );
2385        assert_eq!(
2386            map_sea_type_to_proto("json"),
2387            ProtoType::Message("google.protobuf.Struct".to_string())
2388        );
2389
2390        // Empty type
2391        assert_eq!(
2392            map_sea_type_to_proto("void"),
2393            ProtoType::Message("google.protobuf.Empty".to_string())
2394        );
2395
2396        // Nullable/optional types
2397        assert_eq!(
2398            map_sea_type_to_proto("optional_string"),
2399            ProtoType::Message("google.protobuf.StringValue".to_string())
2400        );
2401        assert_eq!(
2402            map_sea_type_to_proto("nullable_int"),
2403            ProtoType::Message("google.protobuf.Int64Value".to_string())
2404        );
2405    }
2406
2407    #[test]
2408    fn test_add_wkt_imports() {
2409        let mut proto = ProtoFile::new("test");
2410
2411        // Add a message with a Timestamp field
2412        let mut msg = ProtoMessage::new("Event");
2413        msg.fields.push(ProtoField {
2414            name: "created_at".to_string(),
2415            number: 1,
2416            proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
2417            repeated: false,
2418            optional: false,
2419            comments: vec![],
2420        });
2421        msg.fields.push(ProtoField {
2422            name: "duration".to_string(),
2423            number: 2,
2424            proto_type: ProtoType::Message("google.protobuf.Duration".to_string()),
2425            repeated: false,
2426            optional: false,
2427            comments: vec![],
2428        });
2429        proto.messages.push(msg);
2430
2431        proto.add_wkt_imports();
2432
2433        assert!(proto
2434            .imports
2435            .contains(&"google/protobuf/timestamp.proto".to_string()));
2436        assert!(proto
2437            .imports
2438            .contains(&"google/protobuf/duration.proto".to_string()));
2439    }
2440
2441    #[test]
2442    fn test_resolve_imports_in_message() {
2443        // Setup index with a target message
2444        let mut index = HashMap::new();
2445        index.insert(
2446            "TargetType".to_string(),
2447            ("other.ns".to_string(), "base.other.ns".to_string()),
2448        );
2449
2450        let mut msg = ProtoMessage::new("SourceMessage");
2451        msg.fields.push(ProtoField {
2452            name: "field1".to_string(),
2453            number: 1,
2454            proto_type: ProtoType::Message("TargetType".to_string()),
2455            repeated: false,
2456            optional: false,
2457            comments: vec![],
2458        });
2459
2460        let mut imports = HashSet::new();
2461
2462        // Resolve imports
2463        ProtobufEngine::resolve_imports_in_message(&mut msg, "current.ns", &index, &mut imports);
2464
2465        // Should find import for other.ns
2466        assert!(imports.contains("other.ns"));
2467
2468        // Should update type name to fully qualified
2469        let field_type = if let ProtoType::Message(ref name) = msg.fields[0].proto_type {
2470            name.clone()
2471        } else {
2472            panic!("Wrong type");
2473        };
2474        assert_eq!(field_type, "base.other.ns.TargetType");
2475    }
2476
2477    #[test]
2478    fn test_resolve_imports_in_nested_message() {
2479        let mut index = HashMap::new();
2480        index.insert(
2481            "NestedTarget".to_string(),
2482            ("other.ns".to_string(), "base.other.ns".to_string()),
2483        );
2484
2485        let mut msg = ProtoMessage::new("Outer");
2486        let mut inner = ProtoMessage::new("Inner");
2487        inner.fields.push(ProtoField {
2488            name: "field".to_string(),
2489            number: 1,
2490            proto_type: ProtoType::Message("NestedTarget".to_string()),
2491            repeated: false,
2492            optional: false,
2493            comments: vec![],
2494        });
2495        msg.nested_messages.push(inner);
2496
2497        let mut imports = HashSet::new();
2498        ProtobufEngine::resolve_imports_in_message(&mut msg, "current.ns", &index, &mut imports);
2499
2500        assert!(imports.contains("other.ns"));
2501    }
2502
2503    #[test]
2504    fn test_wkt_imports_in_proto_string() {
2505        let mut proto = ProtoFile::new("test.wkt");
2506
2507        let mut msg = ProtoMessage::new("AuditLog");
2508        msg.fields.push(ProtoField {
2509            name: "timestamp".to_string(),
2510            number: 1,
2511            proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
2512            repeated: false,
2513            optional: false,
2514            comments: vec![],
2515        });
2516        proto.messages.push(msg);
2517        proto.add_wkt_imports();
2518
2519        let output = proto.to_proto_string();
2520        assert!(output.contains("import \"google/protobuf/timestamp.proto\";"));
2521    }
2522
2523    #[test]
2524    fn test_wkt_no_duplicate_imports() {
2525        let mut proto = ProtoFile::new("test");
2526
2527        // Add multiple messages using the same WKT
2528        let mut msg1 = ProtoMessage::new("Event1");
2529        msg1.fields.push(ProtoField {
2530            name: "time1".to_string(),
2531            number: 1,
2532            proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
2533            repeated: false,
2534            optional: false,
2535            comments: vec![],
2536        });
2537
2538        let mut msg2 = ProtoMessage::new("Event2");
2539        msg2.fields.push(ProtoField {
2540            name: "time2".to_string(),
2541            number: 1,
2542            proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
2543            repeated: false,
2544            optional: false,
2545            comments: vec![],
2546        });
2547
2548        proto.messages.push(msg1);
2549        proto.messages.push(msg2);
2550        proto.add_wkt_imports();
2551
2552        // Should only have one timestamp import
2553        let timestamp_count = proto
2554            .imports
2555            .iter()
2556            .filter(|i| i.contains("timestamp"))
2557            .count();
2558        assert_eq!(timestamp_count, 1);
2559    }
2560
2561    // ========================================================================
2562    // Custom Options Tests
2563    // ========================================================================
2564
2565    #[test]
2566    fn test_proto_option_value_string() {
2567        let val = ProtoOptionValue::String("com.example.api".to_string());
2568        assert_eq!(val.to_proto_string(), "\"com.example.api\"");
2569    }
2570
2571    #[test]
2572    fn test_proto_option_value_string_escaping() {
2573        let val = ProtoOptionValue::String("path\\to\\file".to_string());
2574        assert_eq!(val.to_proto_string(), "\"path\\\\to\\\\file\"");
2575
2576        let val2 = ProtoOptionValue::String("say \"hello\"".to_string());
2577        assert_eq!(val2.to_proto_string(), "\"say \\\"hello\\\"\"");
2578    }
2579
2580    #[test]
2581    fn test_proto_option_value_int() {
2582        let val = ProtoOptionValue::Int(42);
2583        assert_eq!(val.to_proto_string(), "42");
2584
2585        let neg = ProtoOptionValue::Int(-100);
2586        assert_eq!(neg.to_proto_string(), "-100");
2587    }
2588
2589    #[test]
2590    fn test_proto_option_value_float() {
2591        let val = ProtoOptionValue::Float(3.15);
2592        assert_eq!(val.to_proto_string(), "3.15");
2593    }
2594
2595    #[test]
2596    fn test_proto_option_value_bool() {
2597        assert_eq!(ProtoOptionValue::Bool(true).to_proto_string(), "true");
2598        assert_eq!(ProtoOptionValue::Bool(false).to_proto_string(), "false");
2599    }
2600
2601    #[test]
2602    fn test_proto_option_value_identifier() {
2603        let val = ProtoOptionValue::Identifier("SPEED".to_string());
2604        assert_eq!(val.to_proto_string(), "SPEED");
2605    }
2606
2607    #[test]
2608    fn test_proto_custom_option_to_string() {
2609        let opt = ProtoCustomOption::new(
2610            "java_package",
2611            ProtoOptionValue::String("com.example".to_string()),
2612        );
2613        assert_eq!(
2614            opt.to_proto_string(),
2615            "option java_package = \"com.example\";"
2616        );
2617    }
2618
2619    #[test]
2620    fn test_proto_custom_option_extension() {
2621        // Extension option with parentheses
2622        let opt = ProtoCustomOption::new("(mycompany.api_version)", ProtoOptionValue::Int(2));
2623        assert_eq!(opt.to_proto_string(), "option (mycompany.api_version) = 2;");
2624    }
2625
2626    #[test]
2627    fn test_proto_options_set_standard_options() {
2628        let mut opts = ProtoOptions::default();
2629
2630        opts.set_option(
2631            "java_package",
2632            ProtoOptionValue::String("com.example".to_string()),
2633        );
2634        opts.set_option("java_multiple_files", ProtoOptionValue::Bool(true));
2635        opts.set_option(
2636            "go_package",
2637            ProtoOptionValue::String("github.com/example".to_string()),
2638        );
2639        opts.set_option(
2640            "csharp_namespace",
2641            ProtoOptionValue::String("Example.Api".to_string()),
2642        );
2643        opts.set_option("deprecated", ProtoOptionValue::Bool(true));
2644
2645        assert_eq!(opts.java_package, Some("com.example".to_string()));
2646        assert!(opts.java_multiple_files);
2647        assert_eq!(opts.go_package, Some("github.com/example".to_string()));
2648        assert_eq!(opts.csharp_namespace, Some("Example.Api".to_string()));
2649        assert!(opts.deprecated);
2650    }
2651
2652    #[test]
2653    fn test_proto_options_set_custom_option() {
2654        let mut opts = ProtoOptions::default();
2655
2656        opts.set_option(
2657            "my_custom_option",
2658            ProtoOptionValue::String("custom_value".to_string()),
2659        );
2660
2661        assert_eq!(opts.custom_options.len(), 1);
2662        assert_eq!(opts.custom_options[0].name, "my_custom_option");
2663    }
2664
2665    #[test]
2666    fn test_proto_file_with_all_options() {
2667        let mut proto = ProtoFile::new("test.api");
2668        proto.options.java_package = Some("com.example.api".to_string());
2669        proto.options.java_multiple_files = true;
2670        proto.options.go_package = Some("github.com/example/api".to_string());
2671        proto.options.csharp_namespace = Some("Example.Api".to_string());
2672        proto.options.optimize_for = Some("SPEED".to_string());
2673        proto.options.custom_options.push(ProtoCustomOption::new(
2674            "(api.version)",
2675            ProtoOptionValue::Int(1),
2676        ));
2677
2678        let output = proto.to_proto_string();
2679        assert!(output.contains("option java_package = \"com.example.api\";"));
2680        assert!(output.contains("option java_multiple_files = true;"));
2681        assert!(output.contains("option go_package = \"github.com/example/api\";"));
2682        assert!(output.contains("option csharp_namespace = \"Example.Api\";"));
2683        assert!(output.contains("option optimize_for = SPEED;"));
2684        assert!(output.contains("option (api.version) = 1;"));
2685    }
2686
2687    #[test]
2688    fn test_proto_option_value_from_json() {
2689        use serde_json::json;
2690
2691        assert_eq!(
2692            ProtoOptionValue::from_json(&json!("hello")),
2693            ProtoOptionValue::String("hello".to_string())
2694        );
2695        assert_eq!(
2696            ProtoOptionValue::from_json(&json!(true)),
2697            ProtoOptionValue::Bool(true)
2698        );
2699        assert_eq!(
2700            ProtoOptionValue::from_json(&json!(42)),
2701            ProtoOptionValue::Int(42)
2702        );
2703        assert_eq!(
2704            ProtoOptionValue::from_json(&json!(3.15)),
2705            ProtoOptionValue::Float(3.15)
2706        );
2707    }
2708
2709    // ========================================================================
2710    // gRPC Service Tests
2711    // ========================================================================
2712
2713    #[test]
2714    fn test_proto_service_to_string() {
2715        let mut service = ProtoService::new("PaymentService");
2716        service
2717            .comments
2718            .push("Payment processing service".to_string());
2719
2720        service.methods.push(ProtoRpcMethod::new(
2721            "ProcessPayment",
2722            "PaymentRequest",
2723            "PaymentResponse",
2724        ));
2725
2726        let output = service.to_proto_string();
2727        assert!(output.contains("service PaymentService {"));
2728        assert!(output.contains("rpc ProcessPayment(PaymentRequest) returns (PaymentResponse);"));
2729        assert!(output.contains("// Payment processing service"));
2730    }
2731
2732    #[test]
2733    fn test_proto_rpc_method_unary() {
2734        let method = ProtoRpcMethod::new("GetUser", "GetUserRequest", "User");
2735        let output = method.to_proto_string();
2736        assert_eq!(output, "rpc GetUser(GetUserRequest) returns (User);");
2737    }
2738
2739    #[test]
2740    fn test_proto_rpc_method_server_streaming() {
2741        let mut method = ProtoRpcMethod::new("ListEvents", "ListEventsRequest", "Event");
2742        method.streaming = StreamingMode::ServerStreaming;
2743        let output = method.to_proto_string();
2744        assert_eq!(
2745            output,
2746            "rpc ListEvents(ListEventsRequest) returns (stream Event);"
2747        );
2748    }
2749
2750    #[test]
2751    fn test_proto_rpc_method_client_streaming() {
2752        let mut method = ProtoRpcMethod::new("UploadChunks", "DataChunk", "UploadResult");
2753        method.streaming = StreamingMode::ClientStreaming;
2754        let output = method.to_proto_string();
2755        assert_eq!(
2756            output,
2757            "rpc UploadChunks(stream DataChunk) returns (UploadResult);"
2758        );
2759    }
2760
2761    #[test]
2762    fn test_proto_rpc_method_bidirectional() {
2763        let mut method = ProtoRpcMethod::new("Chat", "ChatMessage", "ChatMessage");
2764        method.streaming = StreamingMode::Bidirectional;
2765        let output = method.to_proto_string();
2766        assert_eq!(
2767            output,
2768            "rpc Chat(stream ChatMessage) returns (stream ChatMessage);"
2769        );
2770    }
2771
2772    #[test]
2773    fn test_streaming_mode_from_str() {
2774        assert_eq!(
2775            StreamingMode::parse("streaming"),
2776            StreamingMode::ServerStreaming
2777        );
2778        assert_eq!(
2779            StreamingMode::parse("server_streaming"),
2780            StreamingMode::ServerStreaming
2781        );
2782        assert_eq!(
2783            StreamingMode::parse("client_streaming"),
2784            StreamingMode::ClientStreaming
2785        );
2786        assert_eq!(
2787            StreamingMode::parse("bidirectional"),
2788            StreamingMode::Bidirectional
2789        );
2790        assert_eq!(StreamingMode::parse("bidi"), StreamingMode::Bidirectional);
2791        assert_eq!(StreamingMode::parse("duplex"), StreamingMode::Bidirectional);
2792        assert_eq!(StreamingMode::parse("unary"), StreamingMode::Unary);
2793        assert_eq!(StreamingMode::parse(""), StreamingMode::Unary);
2794    }
2795
2796    #[test]
2797    fn test_streaming_mode_display() {
2798        assert_eq!(format!("{}", StreamingMode::Unary), "unary");
2799        assert_eq!(
2800            format!("{}", StreamingMode::ServerStreaming),
2801            "server_streaming"
2802        );
2803        assert_eq!(
2804            format!("{}", StreamingMode::ClientStreaming),
2805            "client_streaming"
2806        );
2807        assert_eq!(format!("{}", StreamingMode::Bidirectional), "bidirectional");
2808    }
2809
2810    #[test]
2811    fn test_proto_file_with_services() {
2812        let mut proto = ProtoFile::new("test.api");
2813
2814        let mut service = ProtoService::new("GreeterService");
2815        service.methods.push(ProtoRpcMethod::new(
2816            "SayHello",
2817            "HelloRequest",
2818            "HelloResponse",
2819        ));
2820        proto.services.push(service);
2821
2822        let output = proto.to_proto_string();
2823        assert!(output.contains("service GreeterService {"));
2824        assert!(output.contains("rpc SayHello(HelloRequest) returns (HelloResponse);"));
2825    }
2826
2827    // ========================================================================
2828    // Compatibility Tests
2829    // ========================================================================
2830
2831    fn make_test_proto(messages: Vec<ProtoMessage>) -> ProtoFile {
2832        ProtoFile {
2833            package: "test.package".to_string(),
2834            syntax: "proto3".to_string(),
2835            imports: vec![],
2836            options: ProtoOptions::default(),
2837            enums: vec![],
2838            messages,
2839            services: vec![],
2840            metadata: ProtoMetadata::default(),
2841        }
2842    }
2843
2844    fn make_test_message(name: &str, fields: Vec<ProtoField>) -> ProtoMessage {
2845        ProtoMessage {
2846            name: name.to_string(),
2847            fields,
2848            nested_messages: vec![],
2849            nested_enums: vec![],
2850            reserved_numbers: vec![],
2851            reserved_names: vec![],
2852            comments: vec![],
2853        }
2854    }
2855
2856    fn make_test_field(name: &str, number: u32, proto_type: ProtoType) -> ProtoField {
2857        ProtoField {
2858            name: name.to_string(),
2859            number,
2860            proto_type,
2861            repeated: false,
2862            optional: false,
2863            comments: vec![],
2864        }
2865    }
2866
2867    #[test]
2868    fn test_compatibility_no_changes() {
2869        let old = make_test_proto(vec![make_test_message(
2870            "Person",
2871            vec![
2872                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
2873                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
2874            ],
2875        )]);
2876
2877        let new = old.clone();
2878
2879        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
2880        assert!(result.is_compatible);
2881        assert!(result.violations.is_empty());
2882    }
2883
2884    #[test]
2885    fn test_compatibility_field_added() {
2886        let old = make_test_proto(vec![make_test_message(
2887            "Person",
2888            vec![make_test_field(
2889                "id",
2890                1,
2891                ProtoType::Scalar(ScalarType::String),
2892            )],
2893        )]);
2894
2895        let new = make_test_proto(vec![make_test_message(
2896            "Person",
2897            vec![
2898                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
2899                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
2900            ],
2901        )]);
2902
2903        // Adding fields is compatible in all modes
2904        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
2905        assert!(result.is_compatible);
2906        assert!(result.violations.is_empty());
2907    }
2908
2909    #[test]
2910    fn test_compatibility_field_removed_additive() {
2911        let old = make_test_proto(vec![make_test_message(
2912            "Person",
2913            vec![
2914                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
2915                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
2916            ],
2917        )]);
2918
2919        let new = make_test_proto(vec![make_test_message(
2920            "Person",
2921            vec![make_test_field(
2922                "id",
2923                1,
2924                ProtoType::Scalar(ScalarType::String),
2925            )],
2926        )]);
2927
2928        // Removing fields is NOT compatible in additive mode
2929        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
2930        assert!(!result.is_compatible);
2931        assert_eq!(result.violations.len(), 1);
2932        assert_eq!(
2933            result.violations[0].violation_type,
2934            ViolationType::FieldRemoved
2935        );
2936    }
2937
2938    #[test]
2939    fn test_compatibility_field_removed_backward() {
2940        let old = make_test_proto(vec![make_test_message(
2941            "Person",
2942            vec![
2943                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
2944                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
2945            ],
2946        )]);
2947
2948        let new = make_test_proto(vec![make_test_message(
2949            "Person",
2950            vec![make_test_field(
2951                "id",
2952                1,
2953                ProtoType::Scalar(ScalarType::String),
2954            )],
2955        )]);
2956
2957        // Removing fields IS compatible in backward mode (with warnings)
2958        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
2959        assert!(result.is_compatible); // Still compatible, just has violations
2960        assert!(!result.violations.is_empty());
2961
2962        // Should suggest reserving the field
2963        assert!(result.suggested_reserved_numbers.contains_key("Person"));
2964        assert!(result.suggested_reserved_numbers["Person"].contains(&2));
2965    }
2966
2967    #[test]
2968    fn test_compatibility_type_change() {
2969        let old = make_test_proto(vec![make_test_message(
2970            "Person",
2971            vec![make_test_field(
2972                "age",
2973                1,
2974                ProtoType::Scalar(ScalarType::Int32),
2975            )],
2976        )]);
2977
2978        let new = make_test_proto(vec![make_test_message(
2979            "Person",
2980            vec![make_test_field(
2981                "age",
2982                1,
2983                ProtoType::Scalar(ScalarType::String),
2984            )],
2985        )]);
2986
2987        // Type changes are NOT compatible in backward mode
2988        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
2989        assert!(!result.is_compatible);
2990        assert!(result
2991            .violations
2992            .iter()
2993            .any(|v| v.violation_type == ViolationType::FieldTypeChanged));
2994    }
2995
2996    #[test]
2997    fn test_compatibility_breaking_mode() {
2998        let old = make_test_proto(vec![make_test_message(
2999            "Person",
3000            vec![make_test_field(
3001                "id",
3002                1,
3003                ProtoType::Scalar(ScalarType::String),
3004            )],
3005        )]);
3006
3007        let new = make_test_proto(vec![make_test_message(
3008            "Person",
3009            vec![make_test_field(
3010                "uuid",
3011                1,
3012                ProtoType::Scalar(ScalarType::Int64),
3013            )],
3014        )]);
3015
3016        // Breaking mode allows everything
3017        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Breaking);
3018        assert!(result.is_compatible);
3019        // Violations are still reported for informational purposes
3020        assert!(!result.violations.is_empty());
3021    }
3022
3023    #[test]
3024    fn test_apply_backward_compatibility() {
3025        let old = make_test_proto(vec![make_test_message(
3026            "Person",
3027            vec![
3028                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
3029                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
3030                make_test_field("email", 3, ProtoType::Scalar(ScalarType::String)),
3031            ],
3032        )]);
3033
3034        let mut new = make_test_proto(vec![make_test_message(
3035            "Person",
3036            vec![
3037                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
3038                // name (2) removed
3039                // email (3) removed
3040                make_test_field("phone", 4, ProtoType::Scalar(ScalarType::String)),
3041            ],
3042        )]);
3043
3044        CompatibilityChecker::apply_backward_compatibility(&old, &mut new);
3045
3046        // Should have added reserved numbers
3047        let person = &new.messages[0];
3048        assert!(person.reserved_numbers.contains(&2));
3049        assert!(person.reserved_numbers.contains(&3));
3050        assert!(person.reserved_names.contains(&"name".to_string()));
3051        assert!(person.reserved_names.contains(&"email".to_string()));
3052    }
3053
3054    #[test]
3055    fn test_compatibility_message_removed() {
3056        let old = make_test_proto(vec![
3057            make_test_message("Person", vec![]),
3058            make_test_message("Address", vec![]),
3059        ]);
3060
3061        let new = make_test_proto(vec![make_test_message("Person", vec![])]);
3062
3063        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
3064        assert!(!result.is_compatible);
3065        assert!(result.violations.iter().any(|v| {
3066            v.violation_type == ViolationType::MessageRemoved && v.message_name == "Address"
3067        }));
3068    }
3069
3070    #[test]
3071    fn test_compatibility_result_report() {
3072        let old = make_test_proto(vec![make_test_message(
3073            "Person",
3074            vec![make_test_field(
3075                "name",
3076                1,
3077                ProtoType::Scalar(ScalarType::String),
3078            )],
3079        )]);
3080
3081        let new = make_test_proto(vec![make_test_message("Person", vec![])]);
3082
3083        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
3084        let report = result.to_report();
3085
3086        assert!(report.contains("Compatibility Check"));
3087        assert!(report.contains("field_removed"));
3088        assert!(report.contains("Person"));
3089    }
3090
3091    #[test]
3092    fn test_compatibility_mode_parsing() {
3093        assert_eq!(
3094            "additive".parse::<CompatibilityMode>().unwrap(),
3095            CompatibilityMode::Additive
3096        );
3097        assert_eq!(
3098            "backward".parse::<CompatibilityMode>().unwrap(),
3099            CompatibilityMode::Backward
3100        );
3101        assert_eq!(
3102            "breaking".parse::<CompatibilityMode>().unwrap(),
3103            CompatibilityMode::Breaking
3104        );
3105        assert!("invalid".parse::<CompatibilityMode>().is_err());
3106    }
3107}