camel_proto_compiler/
lib.rs1mod cache;
7mod compiler;
8
9use std::path::{Path, PathBuf};
10
11pub use cache::ProtoCache;
12pub use compiler::compile_proto;
13use sha2::{Digest, Sha256};
14
15#[derive(Debug, thiserror::Error)]
16pub enum ProtoCompileError {
17 #[error("proto file not found: {0}")]
18 ProtoNotFound(PathBuf),
19 #[error("I/O error: {0}")]
20 Io(#[from] std::io::Error),
21 #[error("vendored protoc unavailable: {0}")]
22 VendoredProtoc(#[from] protoc_bin_vendored::Error),
23 #[error("protoc failed (status: {status:?}): {stderr}")]
24 ProtocFailed { status: Option<i32>, stderr: String },
25 #[error("failed to decode descriptor pool: {0}")]
26 DescriptorDecode(String),
27}
28
29fn hash_proto_content(path: &Path) -> Result<String, ProtoCompileError> {
30 let bytes = std::fs::read(path)?;
31 let mut hasher = Sha256::new();
32 hasher.update(bytes);
33 let digest = hasher.finalize();
34 Ok(digest.iter().map(|b| format!("{b:02x}")).collect())
35}
36
37#[cfg(test)]
38mod tests {
39 use std::path::{Path, PathBuf};
40
41 use tempfile::TempDir;
42
43 use super::*;
44
45 fn test_proto_path() -> PathBuf {
46 Path::new(env!("CARGO_MANIFEST_DIR"))
47 .join("tests")
48 .join("helloworld.proto")
49 }
50
51 #[test]
52 fn compile_proto_success() {
53 let proto = test_proto_path();
54 let pool =
55 compile_proto(&proto, std::iter::empty::<&Path>()).expect("compile should succeed");
56 assert!(
57 pool.get_message_by_name("helloworld.HelloRequest")
58 .is_some()
59 );
60 assert!(pool.get_service_by_name("helloworld.Greeter").is_some());
61 }
62
63 #[test]
64 fn compile_proto_missing_file_returns_error() {
65 let err = compile_proto("/definitely/missing.proto", std::iter::empty::<&Path>())
66 .expect_err("should fail");
67 assert!(matches!(err, ProtoCompileError::ProtoNotFound(_)));
68 }
69
70 #[test]
71 fn compile_proto_invalid_syntax_returns_error() {
72 let tmp = TempDir::new().expect("tmp dir");
73 let bad_proto = tmp.path().join("bad.proto");
74 std::fs::write(
75 &bad_proto,
76 "syntax = \"proto3\";\nmessage Broken { string x = ; }\n",
77 )
78 .expect("write invalid proto");
79
80 let err = compile_proto(&bad_proto, std::iter::once(tmp.path())).expect_err("should fail");
81 assert!(matches!(err, ProtoCompileError::ProtocFailed { .. }));
82 }
83
84 #[test]
85 fn cache_hit_does_not_duplicate_entries() {
86 let cache = ProtoCache::new();
87 let proto = test_proto_path();
88
89 let p1 = cache
90 .get_or_compile(&proto, std::iter::empty::<&Path>())
91 .expect("first compile should succeed");
92 let p2 = cache
93 .get_or_compile(&proto, std::iter::empty::<&Path>())
94 .expect("second compile should hit cache");
95
96 assert!(p1.get_service_by_name("helloworld.Greeter").is_some());
97 assert!(p2.get_service_by_name("helloworld.Greeter").is_some());
98 assert_eq!(cache.len(), 1);
99 }
100
101 #[test]
102 fn cache_does_not_grow_beyond_max() {
103 let cache = ProtoCache::with_max_entries(3);
104 let tmp = tempfile::tempdir().expect("tmp dir");
105
106 for i in 0..4 {
108 let proto = tmp.path().join(format!("pkg{i}.proto"));
109 std::fs::write(
110 &proto,
111 format!(r#"syntax = "proto3"; package pkg{i}; message M {{ string name = 1; }}"#),
112 )
113 .expect("write proto");
114 cache
115 .get_or_compile(&proto, std::iter::once(tmp.path() as &Path))
116 .expect("compile should succeed");
117 }
118
119 assert!(
120 cache.len() <= 3,
121 "cache should respect max_entries, got {}",
122 cache.len()
123 );
124 }
125
126 #[test]
127 fn cache_invalidation_on_content_change() {
128 let cache = ProtoCache::new();
129 let tmp = TempDir::new().expect("tmp dir");
130 let proto = tmp.path().join("demo.proto");
131
132 std::fs::write(
133 &proto,
134 r#"syntax = "proto3";
135package demo;
136message A { string name = 1; }
137"#,
138 )
139 .expect("write proto v1");
140
141 let pool_v1 = cache
142 .get_or_compile(&proto, std::iter::once(tmp.path()))
143 .expect("compile v1");
144 assert!(pool_v1.get_message_by_name("demo.A").is_some());
145 assert_eq!(cache.len(), 1);
146
147 std::fs::write(
148 &proto,
149 r#"syntax = "proto3";
150package demo;
151message A { string name = 1; }
152message B { int32 id = 1; }
153"#,
154 )
155 .expect("write proto v2");
156
157 let pool_v2 = cache
158 .get_or_compile(&proto, std::iter::once(tmp.path()))
159 .expect("compile v2");
160 assert!(pool_v2.get_message_by_name("demo.B").is_some());
161 assert_eq!(cache.len(), 2);
162 }
163}