1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProtoFile {
36 pub package: String,
38 pub syntax: String,
40 pub imports: Vec<String>,
42 pub options: ProtoOptions,
44 pub enums: Vec<ProtoEnum>,
46 pub messages: Vec<ProtoMessage>,
48 pub services: Vec<ProtoService>,
50 pub metadata: ProtoMetadata,
52}
53
54impl ProtoFile {
55 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 pub fn to_proto_string(&self) -> String {
71 let mut out = String::new();
72
73 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 out.push_str(&format!("syntax = \"{}\";\n\n", self.syntax));
94
95 out.push_str(&format!("package {};\n", self.package));
97
98 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 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 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 for e in &self.enums {
162 out.push('\n');
163 out.push_str(&e.to_proto_string());
164 }
165
166 for m in &self.messages {
168 out.push('\n');
169 out.push_str(&m.to_proto_string());
170 }
171
172 for s in &self.services {
174 out.push('\n');
175 out.push_str(&s.to_proto_string());
176 }
177
178 out
179 }
180
181 pub fn add_wkt_imports(&mut self) {
186 use std::collections::HashSet;
187 let mut required_imports: HashSet<&'static str> = HashSet::new();
188
189 for msg in &self.messages {
191 Self::collect_wkt_imports_from_message(msg, &mut required_imports);
192 }
193
194 for import in required_imports {
196 if !self.imports.contains(&import.to_string()) {
197 self.imports.push(import.to_string());
198 }
199 }
200
201 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 for nested in &msg.nested_messages {
219 Self::collect_wkt_imports_from_message(nested, imports);
220 }
221 }
222}
223
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226pub struct ProtoOptions {
227 pub java_package: Option<String>,
229 pub java_multiple_files: bool,
231 pub go_package: Option<String>,
233 pub csharp_namespace: Option<String>,
235 pub php_namespace: Option<String>,
237 pub ruby_package: Option<String>,
239 pub swift_prefix: Option<String>,
241 pub objc_class_prefix: Option<String>,
243 pub optimize_for: Option<String>,
245 pub deprecated: bool,
247 pub custom_options: Vec<ProtoCustomOption>,
249}
250
251impl ProtoOptions {
252 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 self.custom_options.push(ProtoCustomOption {
308 name: name.to_string(),
309 value,
310 });
311 }
312 }
313 }
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
318pub struct ProtoCustomOption {
319 pub name: String,
321 pub value: ProtoOptionValue,
323}
324
325impl ProtoCustomOption {
326 pub fn new(name: impl Into<String>, value: ProtoOptionValue) -> Self {
328 Self {
329 name: name.into(),
330 value,
331 }
332 }
333
334 pub fn to_proto_string(&self) -> String {
336 format!("option {} = {};", self.name, self.value.to_proto_string())
337 }
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
342pub enum ProtoOptionValue {
343 String(String),
345 Int(i64),
347 Float(f64),
349 Bool(bool),
351 Identifier(String),
353}
354
355impl ProtoOptionValue {
356 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
390pub struct ProtoMetadata {
391 pub projection_name: String,
393 pub semantic_version: Option<String>,
395 pub source_namespace: String,
397 pub generated_at: String,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct ProtoService {
408 pub name: String,
410 pub methods: Vec<ProtoRpcMethod>,
412 pub comments: Vec<String>,
414}
415
416impl ProtoService {
417 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 pub fn to_proto_string(&self) -> String {
428 let mut out = String::new();
429
430 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#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct ProtoRpcMethod {
449 pub name: String,
451 pub request_type: String,
453 pub response_type: String,
455 pub streaming: StreamingMode,
457 pub comments: Vec<String>,
459}
460
461impl ProtoRpcMethod {
462 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
499pub enum StreamingMode {
500 #[default]
502 Unary,
503 ServerStreaming,
505 ClientStreaming,
507 Bidirectional,
509}
510
511impl StreamingMode {
512 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#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct ProtoMessage {
537 pub name: String,
539 pub fields: Vec<ProtoField>,
541 pub nested_messages: Vec<ProtoMessage>,
543 pub nested_enums: Vec<ProtoEnum>,
545 pub reserved_numbers: Vec<u32>,
547 pub reserved_names: Vec<String>,
549 pub comments: Vec<String>,
551}
552
553impl ProtoMessage {
554 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 pub fn to_proto_string(&self) -> String {
569 let mut out = String::new();
570
571 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct ProtoField {
623 pub name: String,
625 pub number: u32,
627 pub proto_type: ProtoType,
629 pub repeated: bool,
631 pub optional: bool,
633 pub comments: Vec<String>,
635}
636
637impl ProtoField {
638 pub fn to_proto_string(&self) -> String {
640 let mut parts = Vec::new();
641
642 if !self.comments.is_empty() {
644 }
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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
669pub enum ProtoType {
670 Scalar(ScalarType),
672 Message(String),
674 Enum(String),
676 Map {
678 key: Box<ProtoType>,
679 value: Box<ProtoType>,
680 },
681}
682
683impl ProtoType {
684 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#[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
747pub struct ProtoEnum {
748 pub name: String,
750 pub values: Vec<ProtoEnumValue>,
752 pub comments: Vec<String>,
754}
755
756impl ProtoEnum {
757 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
800pub struct ProtoEnumValue {
801 pub name: String,
803 pub number: i32,
805}
806
807#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
816pub enum WellKnownType {
817 Timestamp,
819 Duration,
821 Any,
823 Struct,
825 Value,
827 ListValue,
829 Empty,
831 Int32Value,
833 Int64Value,
834 UInt32Value,
835 UInt64Value,
836 FloatValue,
837 DoubleValue,
838 BoolValue,
839 StringValue,
840 BytesValue,
841}
842
843impl WellKnownType {
844 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 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 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
912pub fn map_sea_type_to_proto(sea_type: &str) -> ProtoType {
921 match sea_type.to_lowercase().as_str() {
922 "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 "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 "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 _ => ProtoType::Message(to_pascal_case(sea_type)),
977 }
978}
979
980pub 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), Value::Object(_) => ProtoType::Scalar(ScalarType::String), }
996}
997
998pub struct ProtobufEngine;
1007
1008impl ProtobufEngine {
1009 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 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 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 proto.messages.sort_by(|a, b| a.name.cmp(&b.name));
1041
1042 proto.add_wkt_imports();
1044
1045 proto
1046 }
1047
1048 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, )
1064 }
1065
1066 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 proto.add_wkt_imports();
1088
1089 proto
1090 }
1091
1092 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 let mut type_index: HashMap<String, (String, String)> = HashMap::new();
1117
1118 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 if include_services {
1161 let services = Self::flows_to_services(graph, "");
1163 for service in services {
1164 let entity_name = service
1167 .name
1168 .strip_suffix("Service")
1169 .unwrap_or(&service.name);
1170
1171 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 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 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 for (ns, file) in files.iter_mut() {
1205 let mut imports_to_add = HashSet::new();
1206
1207 for msg in &mut file.messages {
1209 Self::resolve_imports_in_message(msg, ns, &type_index, &mut imports_to_add);
1210 }
1211
1212 for svc in &mut file.services {
1214 for method in &mut svc.methods {
1215 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 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 for target_ns in imports_to_add {
1235 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 file.add_wkt_imports();
1248 }
1249
1250 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 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 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 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 for nested in &mut msg.nested_messages {
1306 Self::resolve_imports_in_message(nested, current_ns, index, imports);
1307 }
1308 }
1309 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 if !namespace.is_empty() && flow.namespace() != namespace {
1328 continue;
1329 }
1330
1331 let to_entity = match graph.get_entity(flow.to_id()) {
1333 Some(e) => e,
1334 None => continue, };
1336
1337 let resource = match graph.get_resource(flow.resource_id()) {
1339 Some(r) => r,
1340 None => continue, };
1342
1343 let service_name = format!("{}Service", to_pascal_case(to_entity.name()));
1345
1346 let method_name = format!("Process{}", to_pascal_case(resource.name()));
1348
1349 let request_type = to_pascal_case(resource.name());
1351
1352 let response_type = format!("{}Response", to_pascal_case(resource.name()));
1354
1355 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 let mut method = ProtoRpcMethod::new(&method_name, &request_type, &response_type);
1364 method.streaming = streaming;
1365
1366 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 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 services.into_values().collect()
1393 }
1394
1395 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 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 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 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 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 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 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 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 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 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 fn generate_governance_messages() -> Vec<ProtoMessage> {
1524 let mut messages = Vec::new();
1525
1526 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 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
1620fn 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
1638fn 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
1664fn to_screaming_snake_case(s: &str) -> String {
1666 to_snake_case(s).to_uppercase()
1667}
1668
1669#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1675pub enum CompatibilityMode {
1676 Additive,
1678 #[default]
1680 Backward,
1681 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#[derive(Debug, Clone, Serialize, Deserialize)]
1710pub struct CompatibilityViolation {
1711 pub message_name: String,
1713 pub field_name: Option<String>,
1715 pub field_number: Option<u32>,
1717 pub violation_type: ViolationType,
1719 pub description: String,
1721}
1722
1723#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1725pub enum ViolationType {
1726 FieldRemoved,
1728 FieldNumberReused,
1730 FieldTypeChanged,
1732 FieldRenamed,
1734 MessageRemoved,
1736 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#[derive(Debug, Clone, Serialize, Deserialize)]
1755pub struct CompatibilityResult {
1756 pub is_compatible: bool,
1758 pub mode: CompatibilityMode,
1760 pub violations: Vec<CompatibilityViolation>,
1762 pub suggested_reserved_numbers: BTreeMap<String, Vec<u32>>,
1764 pub suggested_reserved_names: BTreeMap<String, Vec<String>>,
1766}
1767
1768impl CompatibilityResult {
1769 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 pub fn has_violations(&self) -> bool {
1782 !self.violations.is_empty()
1783 }
1784
1785 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
1820pub struct CompatibilityChecker;
1822
1823impl CompatibilityChecker {
1824 pub fn check(old: &ProtoFile, new: &ProtoFile, mode: CompatibilityMode) -> CompatibilityResult {
1834 let mut result = CompatibilityResult::compatible(mode);
1835
1836 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 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 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 result.is_compatible = match mode {
1865 CompatibilityMode::Breaking => true, CompatibilityMode::Backward => {
1867 !result.violations.iter().any(|v| {
1869 matches!(
1870 v.violation_type,
1871 ViolationType::FieldNumberReused | ViolationType::FieldTypeChanged
1872 )
1873 })
1874 }
1875 CompatibilityMode::Additive => {
1876 result.violations.is_empty()
1878 }
1879 };
1880
1881 result
1882 }
1883
1884 fn check_message(old: &ProtoMessage, new: &ProtoMessage, result: &mut CompatibilityResult) {
1886 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 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 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 for (number, new_field) in &new_by_number {
1927 if let Some(old_field) = old_by_number.get(number) {
1928 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 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 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 pub fn apply_backward_compatibility(old: &ProtoFile, new: &mut ProtoFile) {
1982 let result = Self::check(old, new, CompatibilityMode::Backward);
1983
1984 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
2007pub struct SchemaHistory {
2009 history_dir: std::path::PathBuf,
2011}
2012
2013impl SchemaHistory {
2014 pub fn new(history_dir: impl Into<std::path::PathBuf>) -> Self {
2016 Self {
2017 history_dir: history_dir.into(),
2018 }
2019 }
2020
2021 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 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 pub fn save(&self, proto: &ProtoFile) -> Result<(), String> {
2045 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 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 if result.is_compatible || mode == CompatibilityMode::Breaking {
2080 self.save(new)?;
2081 }
2082
2083 Ok(result)
2084 }
2085}
2086
2087#[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 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 let numbers: Vec<u32> = msg.fields.iter().map(|f| f.number).collect();
2263 assert_eq!(numbers, vec![1, 2, 3, 4]); }
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 #[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 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 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 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 assert_eq!(
2392 map_sea_type_to_proto("void"),
2393 ProtoType::Message("google.protobuf.Empty".to_string())
2394 );
2395
2396 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 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 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 ProtobufEngine::resolve_imports_in_message(&mut msg, "current.ns", &index, &mut imports);
2464
2465 assert!(imports.contains("other.ns"));
2467
2468 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 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 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 #[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 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 #[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 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 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 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 let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
2959 assert!(result.is_compatible); assert!(!result.violations.is_empty());
2961
2962 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 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 let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Breaking);
3018 assert!(result.is_compatible);
3019 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 make_test_field("phone", 4, ProtoType::Scalar(ScalarType::String)),
3041 ],
3042 )]);
3043
3044 CompatibilityChecker::apply_backward_compatibility(&old, &mut new);
3045
3046 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}