1use anyhow::{Context, Result, anyhow, bail};
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10use std::fs;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ProtobufSchema {
16 pub package: Option<String>,
18 pub messages: HashMap<String, MessageDef>,
20 pub services: HashMap<String, ServiceDef>,
22 pub enums: HashMap<String, EnumDef>,
24 pub imports: Vec<String>,
26 pub syntax: String,
28 pub description: Option<String>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct MessageDef {
35 pub name: String,
37 pub fields: Vec<FieldDef>,
39 pub nested_messages: HashMap<String, Self>,
41 pub nested_enums: HashMap<String, EnumDef>,
43 pub description: Option<String>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ServiceDef {
50 pub name: String,
52 pub methods: Vec<MethodDef>,
54 pub description: Option<String>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct MethodDef {
61 pub name: String,
63 pub input_type: String,
65 pub output_type: String,
67 pub input_streaming: bool,
69 pub output_streaming: bool,
71 pub description: Option<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct FieldDef {
78 pub name: String,
80 pub number: u32,
82 pub field_type: ProtoType,
84 pub label: FieldLabel,
86 pub default_value: Option<String>,
88 pub description: Option<String>,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94pub enum FieldLabel {
95 None,
97 Repeated,
99 Optional,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct EnumDef {
106 pub name: String,
108 pub values: Vec<EnumValue>,
110 pub description: Option<String>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct EnumValue {
117 pub name: String,
119 pub number: i32,
121 pub description: Option<String>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub enum ProtoType {
128 Double,
129 Float,
130 Int32,
131 Int64,
132 Uint32,
133 Uint64,
134 Sint32,
135 Sint64,
136 Fixed32,
137 Fixed64,
138 Sfixed32,
139 Sfixed64,
140 Bool,
141 String,
142 Bytes,
143 Message(String),
144 Enum(String),
145}
146
147impl ProtoType {
148 #[must_use]
150 pub fn as_str(&self) -> String {
151 match self {
152 Self::Double => "double".to_string(),
153 Self::Float => "float".to_string(),
154 Self::Int32 => "int32".to_string(),
155 Self::Int64 => "int64".to_string(),
156 Self::Uint32 => "uint32".to_string(),
157 Self::Uint64 => "uint64".to_string(),
158 Self::Sint32 => "sint32".to_string(),
159 Self::Sint64 => "sint64".to_string(),
160 Self::Fixed32 => "fixed32".to_string(),
161 Self::Fixed64 => "fixed64".to_string(),
162 Self::Sfixed32 => "sfixed32".to_string(),
163 Self::Sfixed64 => "sfixed64".to_string(),
164 Self::Bool => "bool".to_string(),
165 Self::String => "string".to_string(),
166 Self::Bytes => "bytes".to_string(),
167 Self::Message(name) => name.clone(),
168 Self::Enum(name) => name.clone(),
169 }
170 }
171}
172
173pub fn parse_proto_schema(path: &Path) -> Result<ProtobufSchema> {
181 let content = fs::read_to_string(path).with_context(|| format!("Failed to read proto file: {}", path.display()))?;
182
183 parse_proto_schema_string(&content).with_context(|| format!("Failed to parse proto schema from {}", path.display()))
184}
185
186pub fn parse_proto_schema_with_includes(path: &Path, include_paths: &[PathBuf]) -> Result<ProtobufSchema> {
191 let mut visited = HashSet::new();
192 parse_proto_schema_recursive(path, include_paths, &mut visited)
193}
194
195pub fn parse_proto_schema_string(content: &str) -> Result<ProtobufSchema> {
197 let mut schema = ProtobufSchema {
198 package: None,
199 messages: HashMap::new(),
200 services: HashMap::new(),
201 enums: HashMap::new(),
202 imports: Vec::new(),
203 syntax: String::new(),
204 description: None,
205 };
206
207 schema.syntax = extract_syntax_declaration(content).unwrap_or_else(|| "proto3".to_string());
208
209 if schema.syntax != "proto3" {
210 return Err(anyhow!(
211 "Only proto3 syntax is supported. Found: {}\n\
212 Please convert your proto file to proto3 syntax or use proto3-compatible definitions.\n\
213 See: https://developers.google.com/protocol-buffers/docs/proto3",
214 schema.syntax
215 ));
216 }
217
218 schema.package = extract_package_name(content);
219
220 schema.imports = extract_imports(content);
221
222 parse_top_level_definitions(content, &mut schema)?;
223
224 Ok(schema)
225}
226
227fn parse_proto_schema_recursive(
228 path: &Path,
229 include_paths: &[PathBuf],
230 visited: &mut HashSet<PathBuf>,
231) -> Result<ProtobufSchema> {
232 let visit_key = canonical_or_original(path);
233 if !visited.insert(visit_key) {
234 return Ok(ProtobufSchema {
235 package: None,
236 messages: HashMap::new(),
237 services: HashMap::new(),
238 enums: HashMap::new(),
239 imports: Vec::new(),
240 syntax: "proto3".to_string(),
241 description: None,
242 });
243 }
244
245 let content = fs::read_to_string(path).with_context(|| format!("Failed to read proto file: {}", path.display()))?;
246 let mut schema = parse_proto_schema_string(&content)
247 .with_context(|| format!("Failed to parse proto schema from {}", path.display()))?;
248
249 for import in schema.imports.clone() {
250 let Some(import_path) = resolve_import_path(path, &import, include_paths) else {
251 continue;
252 };
253 let imported_schema = parse_proto_schema_recursive(&import_path, include_paths, visited)?;
254 merge_schema(&mut schema, imported_schema)?;
255 }
256
257 Ok(schema)
258}
259
260fn resolve_import_path(path: &Path, import: &str, include_paths: &[PathBuf]) -> Option<PathBuf> {
261 let mut relative_candidates = path
262 .parent()
263 .into_iter()
264 .map(|parent| parent.join(import))
265 .chain(include_paths.iter().map(|include| include.join(import)));
266
267 relative_candidates
268 .find(|candidate| candidate.is_file())
269 .map(|candidate| canonical_or_original(&candidate))
270}
271
272fn canonical_or_original(path: &Path) -> PathBuf {
273 fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
274}
275
276fn merge_schema(target: &mut ProtobufSchema, imported: ProtobufSchema) -> Result<()> {
277 merge_named_defs("message", &mut target.messages, imported.messages)?;
278 merge_named_defs("enum", &mut target.enums, imported.enums)?;
279 merge_named_defs("service", &mut target.services, imported.services)?;
280
281 for import in imported.imports {
282 if !target.imports.contains(&import) {
283 target.imports.push(import);
284 }
285 }
286
287 Ok(())
288}
289
290fn merge_named_defs<T>(kind: &str, target: &mut HashMap<String, T>, source: HashMap<String, T>) -> Result<()> {
291 for (name, def) in source {
292 if target.contains_key(&name) {
293 bail!("Duplicate {kind} definition found while resolving imports: {name}");
294 }
295 target.insert(name, def);
296 }
297 Ok(())
298}
299
300fn extract_syntax_declaration(content: &str) -> Option<String> {
302 for line in content.lines() {
303 let trimmed = line.trim();
304 if trimmed.starts_with("syntax") {
305 let quote_start = trimmed.find('"')?;
306 let remaining = &trimmed[quote_start + 1..];
307 let quote_end = remaining.find('"')?;
308 return Some(remaining[..quote_end].to_string());
309 }
310 }
311 None
312}
313
314fn extract_package_name(content: &str) -> Option<String> {
316 for line in content.lines() {
317 let trimmed = line.trim();
318 if trimmed.starts_with("package") && !trimmed.starts_with("package ") {
319 continue;
320 }
321 if let Some(package_part) = trimmed.strip_prefix("package ") {
322 let semicolon_pos = package_part.find(';')?;
323 let package_name = package_part[..semicolon_pos].trim();
324 return Some(package_name.to_string());
325 }
326 }
327 None
328}
329
330fn extract_imports(content: &str) -> Vec<String> {
332 let mut imports = Vec::new();
333 for line in content.lines() {
334 let trimmed = line.trim();
335 if trimmed.starts_with("import ") && trimmed.contains('"') {
336 if let Some(quote_start) = trimmed.find('"') {
337 let remaining = &trimmed[quote_start + 1..];
338 if let Some(quote_end) = remaining.find('"') {
339 imports.push(remaining[..quote_end].to_string());
340 }
341 }
342 }
343 }
344 imports
345}
346
347fn parse_top_level_definitions(content: &str, schema: &mut ProtobufSchema) -> Result<()> {
348 let lines: Vec<&str> = content.lines().collect();
349 let mut index = 0;
350 let mut pending_comment: Vec<String> = Vec::new();
351
352 while index < lines.len() {
353 let trimmed = strip_inline_comment(lines[index]).trim();
354
355 if trimmed.is_empty() {
356 if !pending_comment.is_empty() {
357 pending_comment.clear();
358 }
359 index += 1;
360 continue;
361 }
362
363 if let Some(comment) = lines[index].trim().strip_prefix("//") {
364 pending_comment.push(comment.trim().to_string());
365 index += 1;
366 continue;
367 }
368
369 if trimmed.starts_with("message ") {
370 let (message, next_index) = parse_message_block(&lines, index, take_comment(&mut pending_comment))?;
371 schema.messages.insert(message.name.clone(), message);
372 index = next_index;
373 continue;
374 }
375
376 if trimmed.starts_with("enum ") {
377 let (enum_def, next_index) = parse_enum_block(&lines, index, take_comment(&mut pending_comment))?;
378 schema.enums.insert(enum_def.name.clone(), enum_def);
379 index = next_index;
380 continue;
381 }
382
383 if trimmed.starts_with("service ") {
384 let (service, next_index) = parse_service_block(&lines, index, take_comment(&mut pending_comment))?;
385 schema.services.insert(service.name.clone(), service);
386 index = next_index;
387 continue;
388 }
389
390 pending_comment.clear();
391 index += 1;
392 }
393
394 Ok(())
395}
396
397fn parse_message_block(lines: &[&str], start: usize, description: Option<String>) -> Result<(MessageDef, usize)> {
398 let header = strip_inline_comment(lines[start]).trim();
399 let name = extract_block_name(header, "message")
400 .ok_or_else(|| anyhow!("Invalid message declaration: {}", lines[start].trim()))?;
401
402 let mut message = MessageDef {
403 name,
404 fields: Vec::new(),
405 nested_messages: HashMap::new(),
406 nested_enums: HashMap::new(),
407 description,
408 };
409
410 let mut index = start + 1;
411 let mut depth = usize::from(header.contains('{'));
412 let mut pending_comment: Vec<String> = Vec::new();
413
414 while index < lines.len() {
415 let raw_line = lines[index];
416 let line = strip_inline_comment(raw_line);
417 let trimmed = line.trim();
418
419 if trimmed.starts_with("//") {
420 if let Some(comment) = raw_line.trim().strip_prefix("//") {
421 pending_comment.push(comment.trim().to_string());
422 }
423 index += 1;
424 continue;
425 }
426
427 let opens = trimmed.matches('{').count();
428 let closes = trimmed.matches('}').count();
429
430 if depth == 1 && !trimmed.is_empty() && !trimmed.starts_with("message ") && !trimmed.starts_with("enum ") {
431 if let Some(field) = parse_field(trimmed, take_comment(&mut pending_comment))? {
432 message.fields.push(field);
433 }
434 }
435
436 depth += opens;
437 depth = depth.saturating_sub(closes);
438 index += 1;
439
440 if depth == 0 {
441 break;
442 }
443 }
444
445 Ok((message, index))
446}
447
448fn parse_enum_block(lines: &[&str], start: usize, description: Option<String>) -> Result<(EnumDef, usize)> {
449 let header = strip_inline_comment(lines[start]).trim();
450 let name = extract_block_name(header, "enum")
451 .ok_or_else(|| anyhow!("Invalid enum declaration: {}", lines[start].trim()))?;
452
453 let mut enum_def = EnumDef {
454 name,
455 values: Vec::new(),
456 description,
457 };
458
459 let mut index = start + 1;
460 let mut depth = usize::from(header.contains('{'));
461 let mut pending_comment: Vec<String> = Vec::new();
462
463 while index < lines.len() {
464 let raw_line = lines[index];
465 let line = strip_inline_comment(raw_line);
466 let trimmed = line.trim();
467
468 if trimmed.starts_with("//") {
469 if let Some(comment) = raw_line.trim().strip_prefix("//") {
470 pending_comment.push(comment.trim().to_string());
471 }
472 index += 1;
473 continue;
474 }
475
476 let opens = trimmed.matches('{').count();
477 let closes = trimmed.matches('}').count();
478
479 if depth == 1 && trimmed.contains('=') && trimmed.ends_with(';') {
480 if let Some(value) = parse_enum_value(trimmed, take_comment(&mut pending_comment))? {
481 enum_def.values.push(value);
482 }
483 }
484
485 depth += opens;
486 depth = depth.saturating_sub(closes);
487 index += 1;
488
489 if depth == 0 {
490 break;
491 }
492 }
493
494 Ok((enum_def, index))
495}
496
497fn parse_service_block(lines: &[&str], start: usize, description: Option<String>) -> Result<(ServiceDef, usize)> {
498 let header = strip_inline_comment(lines[start]).trim();
499 let name = extract_block_name(header, "service")
500 .ok_or_else(|| anyhow!("Invalid service declaration: {}", lines[start].trim()))?;
501
502 let mut service = ServiceDef {
503 name,
504 methods: Vec::new(),
505 description,
506 };
507
508 let mut index = start + 1;
509 let mut depth = usize::from(header.contains('{'));
510 let mut pending_comment: Vec<String> = Vec::new();
511
512 while index < lines.len() {
513 let raw_line = lines[index];
514 let line = strip_inline_comment(raw_line);
515 let trimmed = line.trim();
516
517 if trimmed.starts_with("//") {
518 if let Some(comment) = raw_line.trim().strip_prefix("//") {
519 pending_comment.push(comment.trim().to_string());
520 }
521 index += 1;
522 continue;
523 }
524
525 let opens = trimmed.matches('{').count();
526 let closes = trimmed.matches('}').count();
527
528 if depth == 1 && trimmed.starts_with("rpc ") {
529 if let Some(method) = parse_rpc_method(trimmed, take_comment(&mut pending_comment))? {
530 service.methods.push(method);
531 }
532 }
533
534 depth += opens;
535 depth = depth.saturating_sub(closes);
536 index += 1;
537
538 if depth == 0 {
539 break;
540 }
541 }
542
543 Ok((service, index))
544}
545
546fn parse_field(line: &str, description: Option<String>) -> Result<Option<FieldDef>> {
547 if !line.ends_with(';') || line.starts_with("option ") || line.starts_with("reserved ") {
548 return Ok(None);
549 }
550
551 let without_semicolon = line.trim_end_matches(';');
552 let declaration = without_semicolon.split('[').next().unwrap_or(without_semicolon).trim();
553 let parts: Vec<&str> = declaration.split_whitespace().collect();
554
555 if parts.len() < 4 {
556 return Ok(None);
557 }
558
559 let (label, type_index) = match parts[0] {
560 "repeated" => (FieldLabel::Repeated, 1),
561 "optional" => (FieldLabel::Optional, 1),
562 _ => (FieldLabel::None, 0),
563 };
564
565 if parts.len() <= type_index + 2 {
566 return Ok(None);
567 }
568
569 let field_type = parse_proto_type(parts[type_index]);
570 let field_name = parts[type_index + 1].to_string();
571 let number = parts[type_index + 3]
572 .parse::<u32>()
573 .with_context(|| format!("Invalid field number in line: {line}"))?;
574
575 Ok(Some(FieldDef {
576 name: field_name,
577 number,
578 field_type,
579 label,
580 default_value: None,
581 description,
582 }))
583}
584
585fn parse_enum_value(line: &str, description: Option<String>) -> Result<Option<EnumValue>> {
586 let without_semicolon = line.trim_end_matches(';').trim();
587 let (name, number) = without_semicolon
588 .split_once('=')
589 .ok_or_else(|| anyhow!("Invalid enum value declaration: {line}"))?;
590
591 Ok(Some(EnumValue {
592 name: name.trim().to_string(),
593 number: number
594 .trim()
595 .parse::<i32>()
596 .with_context(|| format!("Invalid enum value number in line: {line}"))?,
597 description,
598 }))
599}
600
601fn parse_rpc_method(line: &str, description: Option<String>) -> Result<Option<MethodDef>> {
602 let without_semicolon = line.trim_end_matches(';').trim();
603 let after_rpc = without_semicolon
604 .strip_prefix("rpc ")
605 .ok_or_else(|| anyhow!("Invalid RPC declaration: {line}"))?;
606 let method_name_end = after_rpc
607 .find('(')
608 .ok_or_else(|| anyhow!("Invalid RPC declaration: {line}"))?;
609 let method_name = after_rpc[..method_name_end].trim().to_string();
610 let rest = &after_rpc[method_name_end + 1..];
611 let request_end = rest
612 .find(')')
613 .ok_or_else(|| anyhow!("Invalid RPC request declaration: {line}"))?;
614 let request_decl = rest[..request_end].trim();
615 let after_request = rest[request_end + 1..].trim();
616 let returns_decl = after_request
617 .strip_prefix("returns")
618 .ok_or_else(|| anyhow!("Invalid RPC returns declaration: {line}"))?
619 .trim();
620 let returns_decl = returns_decl
621 .strip_prefix('(')
622 .ok_or_else(|| anyhow!("Invalid RPC returns declaration: {line}"))?;
623 let response_end = returns_decl
624 .find(')')
625 .ok_or_else(|| anyhow!("Invalid RPC returns declaration: {line}"))?;
626 let response_decl = returns_decl[..response_end].trim();
627
628 let (input_streaming, input_type) = parse_streaming_type(request_decl);
629 let (output_streaming, output_type) = parse_streaming_type(response_decl);
630
631 Ok(Some(MethodDef {
632 name: method_name,
633 input_type,
634 output_type,
635 input_streaming,
636 output_streaming,
637 description,
638 }))
639}
640
641fn parse_streaming_type(declaration: &str) -> (bool, String) {
642 if let Some(rest) = declaration.strip_prefix("stream ") {
643 (true, rest.trim().to_string())
644 } else {
645 (false, declaration.trim().to_string())
646 }
647}
648
649fn extract_block_name(header: &str, keyword: &str) -> Option<String> {
650 header
651 .strip_prefix(keyword)?
652 .trim()
653 .strip_suffix('{')
654 .unwrap_or_else(|| header.strip_prefix(keyword).unwrap().trim())
655 .split_whitespace()
656 .next()
657 .map(std::string::ToString::to_string)
658}
659
660fn strip_inline_comment(line: &str) -> &str {
661 if let Some((before, _)) = line.split_once("//") {
662 before
663 } else {
664 line
665 }
666}
667
668fn take_comment(pending_comment: &mut Vec<String>) -> Option<String> {
669 if pending_comment.is_empty() {
670 None
671 } else {
672 let comment = pending_comment.join(" ");
673 pending_comment.clear();
674 Some(comment)
675 }
676}
677
678#[allow(dead_code)]
680fn parse_proto_type(type_str: &str) -> ProtoType {
681 match type_str {
682 "double" => ProtoType::Double,
683 "float" => ProtoType::Float,
684 "int32" => ProtoType::Int32,
685 "int64" => ProtoType::Int64,
686 "uint32" => ProtoType::Uint32,
687 "uint64" => ProtoType::Uint64,
688 "sint32" => ProtoType::Sint32,
689 "sint64" => ProtoType::Sint64,
690 "fixed32" => ProtoType::Fixed32,
691 "fixed64" => ProtoType::Fixed64,
692 "sfixed32" => ProtoType::Sfixed32,
693 "sfixed64" => ProtoType::Sfixed64,
694 "bool" => ProtoType::Bool,
695 "string" => ProtoType::String,
696 "bytes" => ProtoType::Bytes,
697 _ => ProtoType::Message(type_str.to_string()),
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704 use tempfile::tempdir;
705
706 #[test]
707 fn test_parse_simple_proto3_schema() {
708 let proto = r#"syntax = "proto3";
709
710package example;
711
712message User {
713 string id = 1;
714 string name = 2;
715 string email = 3;
716}
717"#;
718
719 let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
720 assert_eq!(schema.syntax, "proto3");
721 assert_eq!(schema.package, Some("example".to_string()));
722 let user = schema.messages.get("User").expect("message should be parsed");
723 assert_eq!(user.fields.len(), 3);
724 assert_eq!(user.fields[0].name, "id");
725 }
726
727 #[test]
728 fn test_parse_proto_with_imports() {
729 let proto = r#"syntax = "proto3";
730
731import "google/protobuf/timestamp.proto";
732import "other.proto";
733
734package example;
735"#;
736
737 let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
738 assert_eq!(schema.imports.len(), 2);
739 assert!(schema.imports.contains(&"google/protobuf/timestamp.proto".to_string()));
740 assert!(schema.imports.contains(&"other.proto".to_string()));
741 }
742
743 #[test]
744 fn test_parse_proto_schema_with_includes_merges_imported_messages() {
745 let temp_dir = tempdir().expect("temp dir");
746 let shared_dir = temp_dir.path().join("common");
747 fs::create_dir_all(&shared_dir).expect("create include dir");
748
749 let shared_proto = shared_dir.join("types.proto");
750 fs::write(
751 &shared_proto,
752 r#"syntax = "proto3";
753
754package common;
755
756message SharedType {
757 string id = 1;
758}
759"#,
760 )
761 .expect("write shared proto");
762
763 let root_proto = temp_dir.path().join("service.proto");
764 fs::write(
765 &root_proto,
766 r#"syntax = "proto3";
767
768import "common/types.proto";
769
770package example;
771
772message UsesShared {
773 SharedType shared = 1;
774}
775"#,
776 )
777 .expect("write root proto");
778
779 let schema = parse_proto_schema_with_includes(&root_proto, &[temp_dir.path().to_path_buf()])
780 .expect("schema should resolve imports");
781
782 assert!(schema.messages.contains_key("UsesShared"));
783 assert!(schema.messages.contains_key("SharedType"));
784 assert!(schema.imports.contains(&"common/types.proto".to_string()));
785 }
786
787 #[test]
788 fn test_reject_proto2_syntax() {
789 let proto = r#"syntax = "proto2";
790
791package example;
792
793message User {
794 required string id = 1;
795}
796"#;
797
798 let result = parse_proto_schema_string(proto);
799 assert!(result.is_err());
800 let error_msg = format!("{}", result.unwrap_err());
801 assert!(error_msg.contains("Only proto3 syntax is supported"));
802 assert!(error_msg.contains("proto2"));
803 }
804
805 #[test]
806 fn test_parse_proto_type_scalars() {
807 assert_eq!(parse_proto_type("double"), ProtoType::Double);
808 assert_eq!(parse_proto_type("float"), ProtoType::Float);
809 assert_eq!(parse_proto_type("int32"), ProtoType::Int32);
810 assert_eq!(parse_proto_type("int64"), ProtoType::Int64);
811 assert_eq!(parse_proto_type("bool"), ProtoType::Bool);
812 assert_eq!(parse_proto_type("string"), ProtoType::String);
813 assert_eq!(parse_proto_type("bytes"), ProtoType::Bytes);
814 }
815
816 #[test]
817 fn test_parse_proto_type_message() {
818 match parse_proto_type("User") {
819 ProtoType::Message(name) => assert_eq!(name, "User"),
820 _ => panic!("Expected Message type"),
821 }
822 }
823
824 #[test]
825 fn test_parse_service_and_enum() {
826 let proto = r#"syntax = "proto3";
827
828package example;
829
830enum Status {
831 STATUS_UNKNOWN = 0;
832 STATUS_ACTIVE = 1;
833}
834
835service UserService {
836 rpc GetUser (GetUserRequest) returns (User);
837 rpc ListUsers (ListUsersRequest) returns (stream User);
838}
839"#;
840
841 let schema = parse_proto_schema_string(proto).expect("Failed to parse proto");
842 let status = schema.enums.get("Status").expect("enum should be parsed");
843 assert_eq!(status.values.len(), 2);
844
845 let service = schema.services.get("UserService").expect("service should be parsed");
846 assert_eq!(service.methods.len(), 2);
847 assert_eq!(service.methods[0].name, "GetUser");
848 assert!(service.methods[1].output_streaming);
849 }
850
851 #[test]
852 fn test_proto_type_as_str() {
853 assert_eq!(ProtoType::Double.as_str(), "double");
854 assert_eq!(ProtoType::String.as_str(), "string");
855 assert_eq!(ProtoType::Message("User".to_string()).as_str(), "User");
856 }
857}