camel_dataformat_protobuf/
lib.rs1use std::path::Path;
12
13use bytes::BytesMut;
14use camel_api::body::Body;
15use camel_api::data_format::DataFormat;
16use camel_api::error::CamelError;
17use camel_proto_compiler::{ProtoCache, compile_proto};
18use prost::Message;
19use prost_reflect::{DynamicMessage, MessageDescriptor};
20
21const DEFAULT_MAX_DECODE_BYTES: usize = 64 * 1024 * 1024; #[derive(Debug, Clone, Default, PartialEq, Eq)]
34pub struct ProtobufConfig {
35 pub content_type_format: Option<String>,
38 pub instance_class: Option<String>,
40}
41
42impl ProtobufConfig {
43 pub fn validate(&self) -> Result<(), CamelError> {
44 if let Some(instance_class) = &self.instance_class
45 && instance_class.trim().is_empty()
46 {
47 return Err(CamelError::TypeConversionFailed(
48 "instance_class must be non-empty when set".to_string(),
49 ));
50 }
51
52 Ok(())
53 }
54}
55
56pub struct ProtobufDataFormat {
57 descriptor: MessageDescriptor,
58 max_decode_bytes: usize,
59}
60
61impl ProtobufDataFormat {
62 pub fn new<P: AsRef<Path>>(proto_path: P, message_name: &str) -> Result<Self, CamelError> {
63 let pool =
64 compile_proto(proto_path.as_ref(), std::iter::empty::<&Path>()).map_err(|e| {
65 CamelError::TypeConversionFailed(format!("failed to compile proto: {e}"))
66 })?;
67 let descriptor = pool.get_message_by_name(message_name).ok_or_else(|| {
68 CamelError::Config(format!("message descriptor not found: {message_name}"))
69 })?;
70 Ok(Self {
71 descriptor,
72 max_decode_bytes: DEFAULT_MAX_DECODE_BYTES,
73 })
74 }
75
76 pub fn new_with_cache<P: AsRef<Path>>(
77 proto_path: P,
78 message_name: &str,
79 cache: &ProtoCache,
80 ) -> Result<Self, CamelError> {
81 let pool = cache
82 .get_or_compile(proto_path.as_ref(), std::iter::empty::<&Path>())
83 .map_err(|e| {
84 CamelError::TypeConversionFailed(format!("failed to compile proto: {e}"))
85 })?;
86 let descriptor = pool.get_message_by_name(message_name).ok_or_else(|| {
87 CamelError::Config(format!("message descriptor not found: {message_name}"))
88 })?;
89 Ok(Self {
90 descriptor,
91 max_decode_bytes: DEFAULT_MAX_DECODE_BYTES,
92 })
93 }
94
95 pub fn descriptor(&self) -> &MessageDescriptor {
96 &self.descriptor
97 }
98
99 pub fn max_decode_bytes(&self) -> usize {
101 self.max_decode_bytes
102 }
103
104 #[must_use]
106 pub fn with_max_decode_bytes(mut self, max: usize) -> Self {
107 self.max_decode_bytes = max;
108 self
109 }
110
111 pub fn json_to_dynamic(
112 &self,
113 json_val: serde_json::Value,
114 ) -> Result<DynamicMessage, CamelError> {
115 let json_str = serde_json::to_string(&json_val).map_err(|e| {
116 CamelError::TypeConversionFailed(format!("failed to serialize JSON: {e}"))
117 })?;
118 let mut de = serde_json::Deserializer::from_str(&json_str);
119 DynamicMessage::deserialize(self.descriptor.clone(), &mut de).map_err(|e| {
120 CamelError::TypeConversionFailed(format!("failed to parse JSON into protobuf: {e}"))
121 })
122 }
123
124 pub fn dynamic_to_json(&self, msg: DynamicMessage) -> Result<serde_json::Value, CamelError> {
125 serde_json::to_value(&msg).map_err(|e| {
126 CamelError::TypeConversionFailed(format!("failed to serialize protobuf to JSON: {e}"))
127 })
128 }
129}
130
131impl DataFormat for ProtobufDataFormat {
132 fn name(&self) -> &str {
133 "protobuf"
134 }
135
136 fn marshal(&self, body: Body) -> Result<Body, CamelError> {
137 match body {
138 Body::Json(val) => {
139 let msg = self.json_to_dynamic(val)?;
140 let mut buf = BytesMut::new();
141 msg.encode(&mut buf).map_err(|e| {
142 CamelError::TypeConversionFailed(format!(
143 "failed to encode protobuf message: {e}"
144 ))
145 })?;
146 Ok(Body::Bytes(buf.freeze()))
147 }
148 Body::Text(text) => {
149 let val: serde_json::Value = serde_json::from_str(&text).map_err(|e| {
150 CamelError::TypeConversionFailed(format!(
151 "invalid JSON text for protobuf marshal: {e}"
152 ))
153 })?;
154 self.marshal(Body::Json(val))
155 }
156 Body::Bytes(bytes) => {
157 if bytes.len() > self.max_decode_bytes {
158 return Err(CamelError::TypeConversionFailed(format!(
159 "protobuf marshal rejected: {} bytes exceeds max_decode_bytes {}",
160 bytes.len(),
161 self.max_decode_bytes
162 )));
163 }
164 DynamicMessage::decode(self.descriptor.clone(), bytes.as_ref()).map_err(|e| {
165 CamelError::ProcessorError(format!(
166 "protobuf marshal: invalid bytes for type {}: {e}",
167 self.descriptor.full_name()
168 ))
169 })?;
170 Ok(Body::Bytes(bytes))
171 }
172 Body::Empty => Err(CamelError::TypeConversionFailed(
173 "protobuf marshal does not support empty body".to_string(),
174 )),
175 Body::Stream(_) => Err(CamelError::TypeConversionFailed(
176 "protobuf marshal does not support stream body".to_string(),
177 )),
178 Body::Xml(_) => Err(CamelError::TypeConversionFailed(
179 "protobuf marshal does not support XML body".to_string(),
180 )),
181 }
182 }
183
184 fn unmarshal(&self, body: Body) -> Result<Body, CamelError> {
185 match body {
186 Body::Bytes(bytes) => {
187 if bytes.len() > self.max_decode_bytes {
188 return Err(CamelError::TypeConversionFailed(format!(
189 "protobuf unmarshal rejected: {} bytes exceeds max_decode_bytes {}",
190 bytes.len(),
191 self.max_decode_bytes
192 )));
193 }
194 let msg = DynamicMessage::decode(self.descriptor.clone(), bytes.as_ref()).map_err(
195 |e| {
196 CamelError::TypeConversionFailed(format!(
197 "failed to decode protobuf bytes: {e}"
198 ))
199 },
200 )?;
201 let json = self.dynamic_to_json(msg)?;
202 Ok(Body::Json(json))
203 }
204 Body::Json(val) => Ok(Body::Json(val)),
205 Body::Text(_) => Err(CamelError::TypeConversionFailed(
206 "protobuf unmarshal does not support text body".to_string(),
207 )),
208 Body::Stream(_) => Err(CamelError::TypeConversionFailed(
209 "protobuf unmarshal does not support stream body".to_string(),
210 )),
211 Body::Empty => Err(CamelError::TypeConversionFailed(
212 "protobuf unmarshal does not support empty body".to_string(),
213 )),
214 Body::Xml(_) => Err(CamelError::TypeConversionFailed(
215 "protobuf unmarshal does not support XML body".to_string(),
216 )),
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use std::path::PathBuf;
224
225 use bytes::Bytes;
226 use camel_api::body::Body;
227 use camel_api::data_format::DataFormat;
228 use camel_api::error::CamelError;
229 use serde_json::json;
230
231 use super::{ProtobufConfig, ProtobufDataFormat};
232
233 fn test_proto_path() -> PathBuf {
234 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
235 .join("tests")
236 .join("helloworld.proto")
237 }
238
239 fn data_format() -> ProtobufDataFormat {
240 ProtobufDataFormat::new(test_proto_path(), "helloworld.HelloRequest")
241 .expect("should load descriptor")
242 }
243
244 #[test]
245 fn test_name() {
246 let df = data_format();
247 assert_eq!(df.name(), "protobuf");
248 }
249
250 #[test]
251 fn test_marshal_json_to_bytes() {
252 let df = data_format();
253 let body = Body::Json(json!({ "name": "Alice" }));
254 let out = df.marshal(body).expect("marshal should succeed");
255 match out {
256 Body::Bytes(b) => assert!(!b.is_empty()),
257 other => panic!("expected bytes, got {other:?}"),
258 }
259 }
260
261 #[test]
262 fn test_unmarshal_bytes_to_json() {
263 let df = data_format();
264 let bytes = match df
265 .marshal(Body::Json(json!({ "name": "Alice" })))
266 .expect("marshal should succeed")
267 {
268 Body::Bytes(b) => b,
269 other => panic!("expected bytes, got {other:?}"),
270 };
271
272 let out = df
273 .unmarshal(Body::Bytes(bytes))
274 .expect("unmarshal should succeed");
275 match out {
276 Body::Json(v) => assert_eq!(v, json!({ "name": "Alice" })),
277 other => panic!("expected json, got {other:?}"),
278 }
279 }
280
281 #[test]
282 fn test_roundtrip_json_bytes_json() {
283 let df = data_format();
284 let input = json!({ "name": "Bob" });
285 let bytes = match df
286 .marshal(Body::Json(input.clone()))
287 .expect("marshal should succeed")
288 {
289 Body::Bytes(b) => b,
290 other => panic!("expected bytes, got {other:?}"),
291 };
292 let output = match df
293 .unmarshal(Body::Bytes(bytes))
294 .expect("unmarshal should succeed")
295 {
296 Body::Json(v) => v,
297 other => panic!("expected json, got {other:?}"),
298 };
299 assert_eq!(output, input);
300 }
301
302 #[test]
303 fn test_marshal_bytes_passthrough() {
304 let df = data_format();
305 let body = Body::Bytes(Bytes::from_static(b"raw"));
306 let err = df
307 .marshal(body)
308 .expect_err("invalid bytes should be rejected");
309 assert!(
310 err.to_string()
311 .contains("protobuf marshal: invalid bytes for type")
312 );
313 }
314
315 #[test]
316 fn test_marshal_valid_bytes_accepted() {
317 let df = data_format();
318 let bytes = match df
319 .marshal(Body::Json(json!({ "name": "Alice" })))
320 .expect("marshal should succeed")
321 {
322 Body::Bytes(b) => b,
323 other => panic!("expected bytes, got {other:?}"),
324 };
325 let out = df
326 .marshal(Body::Bytes(bytes.clone()))
327 .expect("valid protobuf bytes should be accepted");
328 assert_eq!(out, Body::Bytes(bytes));
329 }
330
331 #[test]
332 fn test_unmarshal_json_passthrough() {
333 let df = data_format();
334 let body = Body::Json(json!({ "name": "Passthrough" }));
335 let out = df
336 .unmarshal(body.clone())
337 .expect("unmarshal should pass through JSON");
338 assert_eq!(out, body);
339 }
340
341 #[test]
342 fn test_marshal_empty_rejected() {
343 let df = data_format();
344 let err = df.marshal(Body::Empty).expect_err("empty must be rejected");
345 assert!(format!("{err}").contains("empty"));
346 }
347
348 #[test]
349 fn test_unmarshal_empty_rejected() {
350 let df = data_format();
351 let err = df
352 .unmarshal(Body::Empty)
353 .expect_err("empty must be rejected");
354 assert!(format!("{err}").contains("empty"));
355 }
356
357 #[test]
358 fn test_message_not_found_error() {
359 let err = ProtobufDataFormat::new(test_proto_path(), "helloworld.DoesNotExist")
360 .err()
361 .expect("unknown message should fail");
362 assert!(matches!(err, CamelError::Config(_)));
363 }
364
365 #[test]
366 fn test_empty_instance_class_rejected() {
367 let config = ProtobufConfig {
368 instance_class: Some("".into()),
369 ..Default::default()
370 };
371 assert!(config.validate().is_err());
372 }
373
374 #[test]
375 fn test_valid_instance_class_accepted() {
376 let config = ProtobufConfig {
377 instance_class: Some("com.example.MyMessage".into()),
378 ..Default::default()
379 };
380 assert!(config.validate().is_ok());
381 }
382
383 #[test]
384 fn test_no_instance_class_valid() {
385 let config = ProtobufConfig::default();
386 assert!(config.validate().is_ok());
387 }
388
389 #[test]
392 fn test_encode_decode_roundtrip() {
393 let df = data_format();
394 let original = json!({ "name": "RoundtripCharlie" });
395
396 let encoded = match df
398 .marshal(Body::Json(original.clone()))
399 .expect("marshal should succeed")
400 {
401 Body::Bytes(b) => b,
402 other => panic!("expected bytes after marshal, got {other:?}"),
403 };
404 assert!(!encoded.is_empty(), "encoded bytes should not be empty");
405
406 let decoded = match df
408 .unmarshal(Body::Bytes(encoded))
409 .expect("unmarshal should succeed")
410 {
411 Body::Json(v) => v,
412 other => panic!("expected json after unmarshal, got {other:?}"),
413 };
414
415 assert_eq!(decoded, original, "roundtrip should preserve field values");
416 }
417
418 #[test]
419 fn test_unmarshal_rejects_oversized_bytes() {
420 let df = data_format().with_max_decode_bytes(16);
421 let body = Body::Bytes(Bytes::from_static(
423 b"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
424 ));
425 let err = df.unmarshal(body).unwrap_err(); assert!(
427 format!("{err}").contains("max_decode_bytes"),
428 "error should mention max_decode_bytes: {err}"
429 );
430 }
431
432 #[test]
433 fn test_unmarshal_default_cap_accepts_valid_bytes() {
434 let df = data_format(); let bytes = match df
436 .marshal(Body::Json(json!({ "name": "Alice" })))
437 .unwrap() {
439 Body::Bytes(b) => b,
440 _ => panic!("expected bytes"),
441 };
442 let out = df.unmarshal(Body::Bytes(bytes)).unwrap(); assert!(matches!(out, Body::Json(_)));
444 }
445
446 #[test]
447 fn test_with_max_decode_bytes_overrides_default() {
448 let df = data_format().with_max_decode_bytes(1);
449 assert_eq!(df.max_decode_bytes(), 1);
450 }
451
452 fn recursive_node_data_format() -> ProtobufDataFormat {
456 ProtobufDataFormat::new(
457 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
458 .join("tests")
459 .join("fixtures")
460 .join("recursive.proto"),
461 "test.Node",
462 )
463 .expect("recursive.proto must compile and expose test.Node") }
465
466 #[test]
467 fn test_unmarshal_recursive_schema_hits_recursion_limit() {
468 let df = recursive_node_data_format();
478 let mut payload: Vec<u8> = Vec::new();
481 for _ in 0..200 {
482 let mut wrapped = vec![0x0a]; let mut n = payload.len() as u64;
484 loop {
485 let mut byte = (n & 0x7f) as u8;
486 n >>= 7;
487 if n != 0 {
488 byte |= 0x80;
489 }
490 wrapped.push(byte);
491 if n == 0 {
492 break;
493 }
494 }
495 wrapped.extend_from_slice(&payload);
496 payload = wrapped;
497 }
498 assert!(
499 payload.len() < df.max_decode_bytes(),
500 "fixture must sit under byte cap"
501 );
502 let err = df
503 .unmarshal(Body::Bytes(Bytes::from(payload)))
504 .expect_err("deeply-nested recursive payload must be rejected"); let msg = format!("{err}").to_lowercase();
506 assert!(
507 msg.contains("recursion") || msg.contains("limit"),
508 "decode must surface prost's recursion-limit error, got: {msg}"
509 );
510 }
511}