zarja_core/proto/
mod.rs

1//! Proto definition reconstruction module.
2//!
3//! This module provides functionality to reconstruct human-readable `.proto`
4//! source files from parsed `FileDescriptorProto` data.
5//!
6//! ## Architecture
7//!
8//! The reconstruction process is handled by [`ProtoReconstructor`], which:
9//!
10//! 1. Parses raw bytes into a `FileDescriptorProto`
11//! 2. Builds a resolved `FileDescriptor` using prost-reflect
12//! 3. Writes out the `.proto` source using the [`ProtoWriter`] trait
13//!
14//! ## Extensibility
15//!
16//! The [`ProtoWriter`] trait allows customization of how proto elements are written.
17//! This can be used for alternative output formats (JSON, documentation, etc.).
18
19mod writer;
20
21use crate::error::{Error, Result};
22use crate::MAX_FIELD_NUMBER;
23use prost::Message;
24use prost_reflect::{DescriptorPool, FileDescriptor};
25use prost_types::FileDescriptorProto;
26use std::fmt::Write as FmtWrite;
27
28pub use writer::{NullWriter, ProtoWriter, StatsWriter};
29
30/// Configuration for proto reconstruction
31#[derive(Debug, Clone)]
32pub struct ReconstructorConfig {
33    /// Indentation string (default: 2 spaces)
34    pub indent_str: String,
35    /// Include source code comments if available
36    pub include_comments: bool,
37    /// Sort fields by number
38    pub sort_fields: bool,
39}
40
41impl Default for ReconstructorConfig {
42    fn default() -> Self {
43        Self {
44            indent_str: "  ".to_string(),
45            include_comments: true,
46            sort_fields: false,
47        }
48    }
49}
50
51impl ReconstructorConfig {
52    /// Creates a new config with default values
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Sets the indentation string
58    pub fn indent_str(mut self, s: impl Into<String>) -> Self {
59        self.indent_str = s.into();
60        self
61    }
62
63    /// Sets whether to include comments
64    pub fn include_comments(mut self, include: bool) -> Self {
65        self.include_comments = include;
66        self
67    }
68
69    /// Sets whether to sort fields by number
70    pub fn sort_fields(mut self, sort: bool) -> Self {
71        self.sort_fields = sort;
72        self
73    }
74}
75
76/// Proto syntax version
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum ProtoSyntax {
79    /// Proto2 syntax
80    Proto2,
81    /// Proto3 syntax
82    Proto3,
83}
84
85impl ProtoSyntax {
86    /// Returns the syntax declaration string
87    pub fn as_str(&self) -> &'static str {
88        match self {
89            ProtoSyntax::Proto2 => "proto2",
90            ProtoSyntax::Proto3 => "proto3",
91        }
92    }
93}
94
95impl TryFrom<&str> for ProtoSyntax {
96    type Error = Error;
97
98    fn try_from(value: &str) -> Result<Self> {
99        match value {
100            "" | "proto2" => Ok(ProtoSyntax::Proto2),
101            "proto3" => Ok(ProtoSyntax::Proto3),
102            _ => Err(Error::UnsupportedSyntax {
103                syntax: value.to_string(),
104            }),
105        }
106    }
107}
108
109/// Reconstructs proto definitions from FileDescriptorProto
110#[derive(Debug)]
111pub struct ProtoReconstructor {
112    /// The raw FileDescriptorProto
113    proto: FileDescriptorProto,
114    /// The resolved file descriptor
115    descriptor: Option<FileDescriptor>,
116    /// Configuration
117    config: ReconstructorConfig,
118}
119
120impl ProtoReconstructor {
121    /// Creates a new reconstructor from raw bytes
122    pub fn from_bytes(data: &[u8]) -> Result<Self> {
123        let proto = FileDescriptorProto::decode(data)?;
124        Self::from_proto(proto)
125    }
126
127    /// Creates a new reconstructor from a FileDescriptorProto
128    pub fn from_proto(proto: FileDescriptorProto) -> Result<Self> {
129        // Try to build a resolved descriptor
130        let descriptor = Self::build_descriptor(&proto).ok();
131
132        Ok(Self {
133            proto,
134            descriptor,
135            config: ReconstructorConfig::default(),
136        })
137    }
138
139    /// Creates a new reconstructor with custom config
140    pub fn with_config(mut self, config: ReconstructorConfig) -> Self {
141        self.config = config;
142        self
143    }
144
145    /// Try to build a resolved FileDescriptor
146    fn build_descriptor(proto: &FileDescriptorProto) -> Result<FileDescriptor> {
147        // Create a FileDescriptorSet with just our file
148        let fds = prost_types::FileDescriptorSet {
149            file: vec![proto.clone()],
150        };
151
152        let mut fds_bytes = Vec::new();
153        fds.encode(&mut fds_bytes).map_err(|e| {
154            Error::descriptor_build(format!("failed to encode descriptor set: {}", e))
155        })?;
156
157        let pool = DescriptorPool::decode(fds_bytes.as_slice()).map_err(|e| {
158            Error::descriptor_build(format!("failed to decode descriptor pool: {}", e))
159        })?;
160
161        // Get the file descriptor from the pool
162        pool.get_file_by_name(proto.name())
163            .ok_or_else(|| Error::descriptor_build("file not found in pool"))
164    }
165
166    /// Returns the original filename from the descriptor
167    pub fn filename(&self) -> &str {
168        self.proto.name()
169    }
170
171    /// Returns the computed output filename
172    ///
173    /// This parses the go_package option to extract the import path if present.
174    pub fn output_filename(&self) -> String {
175        if let Some(opts) = &self.proto.options {
176            if let Some(go_package) = &opts.go_package {
177                // go_package can be "import/path;package_name" or just "import/path"
178                if let Some(idx) = go_package.find(';') {
179                    let import_path = &go_package[..idx];
180                    let base = std::path::Path::new(self.proto.name())
181                        .file_name()
182                        .and_then(|n| n.to_str())
183                        .unwrap_or(self.proto.name());
184                    return format!("{}/{}", import_path, base);
185                }
186            }
187        }
188        self.proto.name().to_string()
189    }
190
191    /// Returns the proto syntax version
192    pub fn syntax(&self) -> ProtoSyntax {
193        ProtoSyntax::try_from(self.proto.syntax()).unwrap_or(ProtoSyntax::Proto2)
194    }
195
196    /// Returns the resolved file descriptor if available
197    ///
198    /// The descriptor may not be available if the proto has unresolvable dependencies.
199    pub fn file_descriptor(&self) -> Option<&FileDescriptor> {
200        self.descriptor.as_ref()
201    }
202
203    /// Returns the raw FileDescriptorProto
204    pub fn proto(&self) -> &FileDescriptorProto {
205        &self.proto
206    }
207
208    /// Reconstruct the proto definition as a string
209    pub fn reconstruct(&self) -> String {
210        let mut output = String::new();
211        self.write_to(&mut output).expect("String write cannot fail");
212        output
213    }
214
215    /// Write the reconstructed proto to a writer
216    pub fn write_to(&self, w: &mut impl FmtWrite) -> std::fmt::Result {
217        let mut writer = DefaultProtoWriter::new(w, &self.config);
218        writer.write_file(&self.proto, self.syntax())
219    }
220}
221
222/// Default implementation of ProtoWriter
223struct DefaultProtoWriter<'a, W: FmtWrite> {
224    writer: &'a mut W,
225    config: &'a ReconstructorConfig,
226    indent_level: usize,
227}
228
229impl<'a, W: FmtWrite> DefaultProtoWriter<'a, W> {
230    fn new(writer: &'a mut W, config: &'a ReconstructorConfig) -> Self {
231        Self {
232            writer,
233            config,
234            indent_level: 0,
235        }
236    }
237
238    fn indent(&mut self) {
239        self.indent_level += 1;
240    }
241
242    fn dedent(&mut self) {
243        self.indent_level = self.indent_level.saturating_sub(1);
244    }
245
246    fn write_indent(&mut self) -> std::fmt::Result {
247        for _ in 0..self.indent_level {
248            write!(self.writer, "{}", self.config.indent_str)?;
249        }
250        Ok(())
251    }
252
253    fn writeln(&mut self, s: &str) -> std::fmt::Result {
254        self.write_indent()?;
255        writeln!(self.writer, "{}", s)
256    }
257
258    fn write_file(
259        &mut self,
260        proto: &FileDescriptorProto,
261        syntax: ProtoSyntax,
262    ) -> std::fmt::Result {
263        // Syntax declaration
264        writeln!(self.writer, "syntax = \"{}\";", syntax.as_str())?;
265        writeln!(self.writer)?;
266
267        // Package
268        if !proto.package().is_empty() {
269            writeln!(self.writer, "package {};", proto.package())?;
270            writeln!(self.writer)?;
271        }
272
273        // File options
274        self.write_file_options(proto)?;
275
276        // Imports
277        self.write_imports(proto)?;
278
279        // Services
280        for service in &proto.service {
281            self.write_service(service)?;
282        }
283
284        // Messages
285        for message in &proto.message_type {
286            self.write_message(message, syntax)?;
287        }
288
289        // Enums
290        for enum_type in &proto.enum_type {
291            self.write_enum(enum_type)?;
292        }
293
294        // Extensions (top-level)
295        for extension in &proto.extension {
296            self.write_extension(extension, syntax)?;
297        }
298
299        Ok(())
300    }
301
302    fn write_file_options(&mut self, proto: &FileDescriptorProto) -> std::fmt::Result {
303        let Some(opts) = &proto.options else {
304            return Ok(());
305        };
306
307        let mut wrote_option = false;
308
309        // Write known options
310        macro_rules! write_string_option {
311            ($name:expr, $value:expr) => {
312                if let Some(v) = $value {
313                    if !v.is_empty() {
314                        writeln!(self.writer, "option {} = \"{}\";", $name, escape_string(v))?;
315                        wrote_option = true;
316                    }
317                }
318            };
319        }
320
321        macro_rules! write_bool_option {
322            ($name:expr, $value:expr) => {
323                if let Some(v) = $value {
324                    writeln!(self.writer, "option {} = {};", $name, v)?;
325                    wrote_option = true;
326                }
327            };
328        }
329
330        write_string_option!("java_package", opts.java_package.as_ref());
331        write_string_option!("java_outer_classname", opts.java_outer_classname.as_ref());
332        write_bool_option!("java_multiple_files", opts.java_multiple_files);
333        write_bool_option!("java_string_check_utf8", opts.java_string_check_utf8);
334        write_string_option!("go_package", opts.go_package.as_ref());
335        write_bool_option!("cc_enable_arenas", opts.cc_enable_arenas);
336        write_string_option!("objc_class_prefix", opts.objc_class_prefix.as_ref());
337        write_string_option!("csharp_namespace", opts.csharp_namespace.as_ref());
338        write_string_option!("swift_prefix", opts.swift_prefix.as_ref());
339        write_string_option!("php_class_prefix", opts.php_class_prefix.as_ref());
340        write_string_option!("php_namespace", opts.php_namespace.as_ref());
341        write_string_option!("php_metadata_namespace", opts.php_metadata_namespace.as_ref());
342        write_string_option!("ruby_package", opts.ruby_package.as_ref());
343
344        if wrote_option {
345            writeln!(self.writer)?;
346        }
347
348        Ok(())
349    }
350
351    fn write_imports(&mut self, proto: &FileDescriptorProto) -> std::fmt::Result {
352        if proto.dependency.is_empty() {
353            return Ok(());
354        }
355
356        // Build set of public and weak imports
357        let public_deps: std::collections::HashSet<_> =
358            proto.public_dependency.iter().map(|&i| i as usize).collect();
359        let weak_deps: std::collections::HashSet<_> =
360            proto.weak_dependency.iter().map(|&i| i as usize).collect();
361
362        for (i, dep) in proto.dependency.iter().enumerate() {
363            let modifier = if public_deps.contains(&i) {
364                "public "
365            } else if weak_deps.contains(&i) {
366                "weak "
367            } else {
368                ""
369            };
370            writeln!(self.writer, "import {}\"{}\";", modifier, dep)?;
371        }
372
373        writeln!(self.writer)?;
374        Ok(())
375    }
376
377    fn write_service(&mut self, service: &prost_types::ServiceDescriptorProto) -> std::fmt::Result {
378        writeln!(self.writer, "service {} {{", service.name())?;
379        self.indent();
380
381        for method in &service.method {
382            self.write_method(method)?;
383        }
384
385        self.dedent();
386        writeln!(self.writer, "}}")?;
387        writeln!(self.writer)?;
388        Ok(())
389    }
390
391    fn write_method(&mut self, method: &prost_types::MethodDescriptorProto) -> std::fmt::Result {
392        let client_streaming = method.client_streaming.unwrap_or(false);
393        let server_streaming = method.server_streaming.unwrap_or(false);
394
395        let input = if client_streaming {
396            format!("stream {}", method.input_type())
397        } else {
398            method.input_type().to_string()
399        };
400
401        let output = if server_streaming {
402            format!("stream {}", method.output_type())
403        } else {
404            method.output_type().to_string()
405        };
406
407        self.write_indent()?;
408        writeln!(
409            self.writer,
410            "rpc {}({}) returns ({});",
411            method.name(),
412            input,
413            output
414        )?;
415
416        Ok(())
417    }
418
419    fn write_message(
420        &mut self,
421        message: &prost_types::DescriptorProto,
422        syntax: ProtoSyntax,
423    ) -> std::fmt::Result {
424        writeln!(self.writer, "message {} {{", message.name())?;
425        self.indent();
426
427        // Reserved ranges and names
428        self.write_reserved(message)?;
429
430        // Nested messages
431        for nested in &message.nested_type {
432            // Skip map entry types (they're synthetic)
433            if nested.options.as_ref().map_or(false, |o| o.map_entry.unwrap_or(false)) {
434                continue;
435            }
436            self.write_message(nested, syntax)?;
437        }
438
439        // Nested enums
440        for enum_type in &message.enum_type {
441            self.write_enum(enum_type)?;
442        }
443
444        // Collect oneof field indices
445        let mut oneof_fields: std::collections::HashMap<i32, Vec<&prost_types::FieldDescriptorProto>> =
446            std::collections::HashMap::new();
447
448        for field in &message.field {
449            if let Some(oneof_index) = field.oneof_index {
450                // Check if this is a proto3 optional (has synthetic oneof)
451                if !Self::is_proto3_optional(field, message) {
452                    oneof_fields
453                        .entry(oneof_index)
454                        .or_default()
455                        .push(field);
456                }
457            }
458        }
459
460        // Write oneofs
461        for (i, oneof) in message.oneof_decl.iter().enumerate() {
462            if let Some(fields) = oneof_fields.get(&(i as i32)) {
463                if !fields.is_empty() {
464                    self.write_oneof(oneof, fields, syntax)?;
465                }
466            }
467        }
468
469        // Write regular fields (excluding those in oneofs)
470        for field in &message.field {
471            let in_real_oneof = field.oneof_index.is_some()
472                && !Self::is_proto3_optional(field, message)
473                && oneof_fields.contains_key(&field.oneof_index.unwrap());
474
475            if !in_real_oneof {
476                self.write_field(field, syntax, message)?;
477            }
478        }
479
480        // Extensions
481        for extension in &message.extension {
482            self.write_extension(extension, syntax)?;
483        }
484
485        // Extension ranges
486        for range in &message.extension_range {
487            self.write_indent()?;
488            let end = if range.end() == MAX_FIELD_NUMBER as i32 + 1 {
489                "max".to_string()
490            } else {
491                (range.end() - 1).to_string()
492            };
493            writeln!(self.writer, "extensions {} to {};", range.start(), end)?;
494        }
495
496        self.dedent();
497        self.writeln("}")?;
498        writeln!(self.writer)?;
499
500        Ok(())
501    }
502
503    fn is_proto3_optional(
504        field: &prost_types::FieldDescriptorProto,
505        message: &prost_types::DescriptorProto,
506    ) -> bool {
507        // In proto3, optional fields have a synthetic oneof
508        if let Some(oneof_index) = field.oneof_index {
509            if let Some(oneof) = message.oneof_decl.get(oneof_index as usize) {
510                // Synthetic oneofs have names starting with "_"
511                return oneof.name().starts_with('_');
512            }
513        }
514        false
515    }
516
517    fn write_reserved(&mut self, message: &prost_types::DescriptorProto) -> std::fmt::Result {
518        // Reserved names
519        if !message.reserved_name.is_empty() {
520            self.write_indent()?;
521            write!(self.writer, "reserved ")?;
522            for (i, name) in message.reserved_name.iter().enumerate() {
523                if i > 0 {
524                    write!(self.writer, ", ")?;
525                }
526                write!(self.writer, "\"{}\"", name)?;
527            }
528            writeln!(self.writer, ";")?;
529        }
530
531        // Reserved ranges
532        if !message.reserved_range.is_empty() {
533            self.write_indent()?;
534            write!(self.writer, "reserved ")?;
535            for (i, range) in message.reserved_range.iter().enumerate() {
536                if i > 0 {
537                    write!(self.writer, ", ")?;
538                }
539                if range.start() == range.end() - 1 {
540                    write!(self.writer, "{}", range.start())?;
541                } else {
542                    let end = if range.end() == MAX_FIELD_NUMBER as i32 + 1 {
543                        "max".to_string()
544                    } else {
545                        (range.end() - 1).to_string()
546                    };
547                    write!(self.writer, "{} to {}", range.start(), end)?;
548                }
549            }
550            writeln!(self.writer, ";")?;
551        }
552
553        Ok(())
554    }
555
556    fn write_oneof(
557        &mut self,
558        oneof: &prost_types::OneofDescriptorProto,
559        fields: &[&prost_types::FieldDescriptorProto],
560        _syntax: ProtoSyntax,
561    ) -> std::fmt::Result {
562        self.write_indent()?;
563        writeln!(self.writer, "oneof {} {{", oneof.name())?;
564        self.indent();
565
566        for field in fields {
567            self.write_oneof_field(field)?;
568        }
569
570        self.dedent();
571        self.writeln("}")?;
572
573        Ok(())
574    }
575
576    fn write_oneof_field(&mut self, field: &prost_types::FieldDescriptorProto) -> std::fmt::Result {
577        self.write_indent()?;
578        writeln!(
579            self.writer,
580            "{} {} = {};",
581            self.field_type_name(field),
582            field.name(),
583            field.number()
584        )?;
585        Ok(())
586    }
587
588    fn write_field(
589        &mut self,
590        field: &prost_types::FieldDescriptorProto,
591        syntax: ProtoSyntax,
592        message: &prost_types::DescriptorProto,
593    ) -> std::fmt::Result {
594        self.write_indent()?;
595
596        // Determine field label
597        let label = self.field_label(field, syntax, message);
598        if !label.is_empty() {
599            write!(self.writer, "{} ", label)?;
600        }
601
602        // Check for map type
603        if self.is_map_field(field, message) {
604            self.write_map_field(field, message)?;
605        } else {
606            // Regular field
607            write!(
608                self.writer,
609                "{} {} = {}",
610                self.field_type_name(field),
611                field.name(),
612                field.number()
613            )?;
614
615            // Field options (default value, etc.)
616            self.write_field_options(field, syntax)?;
617
618            writeln!(self.writer, ";")?;
619        }
620
621        Ok(())
622    }
623
624    fn is_map_field(
625        &self,
626        field: &prost_types::FieldDescriptorProto,
627        message: &prost_types::DescriptorProto,
628    ) -> bool {
629        if field.label() != prost_types::field_descriptor_proto::Label::Repeated {
630            return false;
631        }
632        if field.r#type() != prost_types::field_descriptor_proto::Type::Message {
633            return false;
634        }
635
636        // Find the nested type
637        let type_name = field.type_name();
638        for nested in &message.nested_type {
639            let expected_name = format!(".{}", nested.name());
640            if type_name.ends_with(&expected_name) || type_name == nested.name() {
641                return nested.options.as_ref().map_or(false, |o| o.map_entry.unwrap_or(false));
642            }
643        }
644
645        false
646    }
647
648    fn write_map_field(
649        &mut self,
650        field: &prost_types::FieldDescriptorProto,
651        message: &prost_types::DescriptorProto,
652    ) -> std::fmt::Result {
653        // Find the map entry type
654        let type_name = field.type_name();
655        for nested in &message.nested_type {
656            let expected_name = format!(".{}", nested.name());
657            if type_name.ends_with(&expected_name) || type_name == nested.name() {
658                if nested.options.as_ref().map_or(false, |o| o.map_entry.unwrap_or(false)) {
659                    // This is a map entry
660                    let key_field = nested.field.iter().find(|f| f.number() == 1);
661                    let value_field = nested.field.iter().find(|f| f.number() == 2);
662
663                    if let (Some(key), Some(value)) = (key_field, value_field) {
664                        writeln!(
665                            self.writer,
666                            "map<{}, {}> {} = {};",
667                            self.field_type_name(key),
668                            self.field_type_name(value),
669                            field.name(),
670                            field.number()
671                        )?;
672                        return Ok(());
673                    }
674                }
675            }
676        }
677
678        // Fallback: just write as a regular field
679        writeln!(
680            self.writer,
681            "{} {} = {};",
682            self.field_type_name(field),
683            field.name(),
684            field.number()
685        )?;
686
687        Ok(())
688    }
689
690    fn field_label(
691        &self,
692        field: &prost_types::FieldDescriptorProto,
693        syntax: ProtoSyntax,
694        message: &prost_types::DescriptorProto,
695    ) -> &'static str {
696        use prost_types::field_descriptor_proto::Label;
697
698        match field.label() {
699            Label::Repeated => {
700                // Check if this is a map (maps don't get "repeated" label)
701                if self.is_map_field(field, message) {
702                    ""
703                } else {
704                    "repeated"
705                }
706            }
707            Label::Required => "required",
708            Label::Optional => {
709                match syntax {
710                    ProtoSyntax::Proto2 => "optional",
711                    ProtoSyntax::Proto3 => {
712                        // In proto3, check if this is an explicit optional (has synthetic oneof)
713                        if Self::is_proto3_optional(field, message) {
714                            "optional"
715                        } else {
716                            ""
717                        }
718                    }
719                }
720            }
721        }
722    }
723
724    fn field_type_name(&self, field: &prost_types::FieldDescriptorProto) -> String {
725        use prost_types::field_descriptor_proto::Type;
726
727        match field.r#type() {
728            Type::Double => "double".to_string(),
729            Type::Float => "float".to_string(),
730            Type::Int64 => "int64".to_string(),
731            Type::Uint64 => "uint64".to_string(),
732            Type::Int32 => "int32".to_string(),
733            Type::Fixed64 => "fixed64".to_string(),
734            Type::Fixed32 => "fixed32".to_string(),
735            Type::Bool => "bool".to_string(),
736            Type::String => "string".to_string(),
737            Type::Bytes => "bytes".to_string(),
738            Type::Uint32 => "uint32".to_string(),
739            Type::Sfixed32 => "sfixed32".to_string(),
740            Type::Sfixed64 => "sfixed64".to_string(),
741            Type::Sint32 => "sint32".to_string(),
742            Type::Sint64 => "sint64".to_string(),
743            Type::Group => "group".to_string(),
744            Type::Message | Type::Enum => {
745                // Return the full type name
746                field.type_name().to_string()
747            }
748        }
749    }
750
751    fn write_field_options(
752        &mut self,
753        field: &prost_types::FieldDescriptorProto,
754        syntax: ProtoSyntax,
755    ) -> std::fmt::Result {
756        let mut options = Vec::new();
757
758        // Default value (proto2 only)
759        if syntax == ProtoSyntax::Proto2 {
760            if let Some(default) = &field.default_value {
761                use prost_types::field_descriptor_proto::Type;
762                let formatted = match field.r#type() {
763                    Type::String => format!("\"{}\"", escape_string(default)),
764                    Type::Bytes => format!("\"{}\"", escape_string(default)),
765                    Type::Enum => default.clone(),
766                    Type::Bool => default.clone(),
767                    _ => default.clone(),
768                };
769                options.push(format!("default = {}", formatted));
770            }
771        }
772
773        // JSON name if different from default
774        if let Some(json_name) = &field.json_name {
775            let default_json_name = to_lower_camel_case(field.name());
776            if json_name != &default_json_name {
777                options.push(format!("json_name = \"{}\"", json_name));
778            }
779        }
780
781        // Packed option
782        if let Some(opts) = &field.options {
783            if let Some(packed) = opts.packed {
784                options.push(format!("packed = {}", packed));
785            }
786            if let Some(deprecated) = opts.deprecated {
787                if deprecated {
788                    options.push("deprecated = true".to_string());
789                }
790            }
791        }
792
793        if !options.is_empty() {
794            write!(self.writer, " [{}]", options.join(", "))?;
795        }
796
797        Ok(())
798    }
799
800    fn write_enum(&mut self, enum_type: &prost_types::EnumDescriptorProto) -> std::fmt::Result {
801        self.write_indent()?;
802        writeln!(self.writer, "enum {} {{", enum_type.name())?;
803        self.indent();
804
805        // Check for allow_alias option
806        if let Some(opts) = &enum_type.options {
807            if opts.allow_alias.unwrap_or(false) {
808                self.writeln("option allow_alias = true;")?;
809            }
810        }
811
812        // Reserved ranges
813        if !enum_type.reserved_range.is_empty() {
814            self.write_indent()?;
815            write!(self.writer, "reserved ")?;
816            for (i, range) in enum_type.reserved_range.iter().enumerate() {
817                if i > 0 {
818                    write!(self.writer, ", ")?;
819                }
820                if range.start() == range.end() {
821                    write!(self.writer, "{}", range.start())?;
822                } else {
823                    let end = if range.end() == i32::MAX {
824                        "max".to_string()
825                    } else {
826                        range.end().to_string()
827                    };
828                    write!(self.writer, "{} to {}", range.start(), end)?;
829                }
830            }
831            writeln!(self.writer, ";")?;
832        }
833
834        // Reserved names
835        if !enum_type.reserved_name.is_empty() {
836            self.write_indent()?;
837            write!(self.writer, "reserved ")?;
838            for (i, name) in enum_type.reserved_name.iter().enumerate() {
839                if i > 0 {
840                    write!(self.writer, ", ")?;
841                }
842                write!(self.writer, "\"{}\"", name)?;
843            }
844            writeln!(self.writer, ";")?;
845        }
846
847        // Values
848        for value in &enum_type.value {
849            self.write_indent()?;
850            write!(self.writer, "{} = {}", value.name(), value.number())?;
851
852            // Value options
853            if let Some(opts) = &value.options {
854                if opts.deprecated.unwrap_or(false) {
855                    write!(self.writer, " [deprecated = true]")?;
856                }
857            }
858
859            writeln!(self.writer, ";")?;
860        }
861
862        self.dedent();
863        self.writeln("}")?;
864        writeln!(self.writer)?;
865
866        Ok(())
867    }
868
869    fn write_extension(
870        &mut self,
871        extension: &prost_types::FieldDescriptorProto,
872        syntax: ProtoSyntax,
873    ) -> std::fmt::Result {
874        self.write_indent()?;
875        writeln!(self.writer, "extend {} {{", extension.extendee())?;
876        self.indent();
877
878        self.write_indent()?;
879
880        // Label
881        use prost_types::field_descriptor_proto::Label;
882        match extension.label() {
883            Label::Repeated => write!(self.writer, "repeated ")?,
884            Label::Required => write!(self.writer, "required ")?,
885            Label::Optional => {
886                if syntax == ProtoSyntax::Proto2 {
887                    write!(self.writer, "optional ")?;
888                }
889            }
890        }
891
892        writeln!(
893            self.writer,
894            "{} {} = {};",
895            self.field_type_name(extension),
896            extension.name(),
897            extension.number()
898        )?;
899
900        self.dedent();
901        self.writeln("}")?;
902        writeln!(self.writer)?;
903
904        Ok(())
905    }
906}
907
908/// Escape a string for proto syntax
909fn escape_string(s: &str) -> String {
910    let mut result = String::with_capacity(s.len());
911    for c in s.chars() {
912        match c {
913            '\\' => result.push_str("\\\\"),
914            '"' => result.push_str("\\\""),
915            '\n' => result.push_str("\\n"),
916            '\r' => result.push_str("\\r"),
917            '\t' => result.push_str("\\t"),
918            _ if c.is_ascii_control() => {
919                result.push_str(&format!("\\x{:02x}", c as u8));
920            }
921            _ => result.push(c),
922        }
923    }
924    result
925}
926
927/// Convert a snake_case name to lowerCamelCase
928fn to_lower_camel_case(s: &str) -> String {
929    let mut result = String::with_capacity(s.len());
930    let mut capitalize_next = false;
931
932    for c in s.chars() {
933        if c == '_' {
934            capitalize_next = true;
935        } else if capitalize_next {
936            result.push(c.to_ascii_uppercase());
937            capitalize_next = false;
938        } else {
939            result.push(c);
940        }
941    }
942
943    result
944}
945
946#[cfg(test)]
947mod tests {
948    use super::*;
949
950    #[test]
951    fn test_escape_string() {
952        assert_eq!(escape_string("hello"), "hello");
953        assert_eq!(escape_string("hello\\world"), "hello\\\\world");
954        assert_eq!(escape_string("hello\"world"), "hello\\\"world");
955        assert_eq!(escape_string("hello\nworld"), "hello\\nworld");
956    }
957
958    #[test]
959    fn test_to_lower_camel_case() {
960        assert_eq!(to_lower_camel_case("hello_world"), "helloWorld");
961        assert_eq!(to_lower_camel_case("my_field_name"), "myFieldName");
962        assert_eq!(to_lower_camel_case("simple"), "simple");
963    }
964
965    #[test]
966    fn test_proto_syntax() {
967        assert_eq!(ProtoSyntax::try_from("").unwrap(), ProtoSyntax::Proto2);
968        assert_eq!(ProtoSyntax::try_from("proto2").unwrap(), ProtoSyntax::Proto2);
969        assert_eq!(ProtoSyntax::try_from("proto3").unwrap(), ProtoSyntax::Proto3);
970        assert!(ProtoSyntax::try_from("proto4").is_err());
971    }
972}