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 _ => Err(CamelError::TypeConversionFailed(
182 "protobuf marshal does not support this body type".to_string(),
183 )),
184 }
185 }
186
187 fn unmarshal(&self, body: Body) -> Result<Body, CamelError> {
188 match body {
189 Body::Bytes(bytes) => {
190 if bytes.len() > self.max_decode_bytes {
191 return Err(CamelError::TypeConversionFailed(format!(
192 "protobuf unmarshal rejected: {} bytes exceeds max_decode_bytes {}",
193 bytes.len(),
194 self.max_decode_bytes
195 )));
196 }
197 let msg = DynamicMessage::decode(self.descriptor.clone(), bytes.as_ref()).map_err(
198 |e| {
199 CamelError::TypeConversionFailed(format!(
200 "failed to decode protobuf bytes: {e}"
201 ))
202 },
203 )?;
204 let json = self.dynamic_to_json(msg)?;
205 Ok(Body::Json(json))
206 }
207 Body::Json(val) => Ok(Body::Json(val)),
208 Body::Text(_) => Err(CamelError::TypeConversionFailed(
209 "protobuf unmarshal does not support text body".to_string(),
210 )),
211 Body::Stream(_) => Err(CamelError::TypeConversionFailed(
212 "protobuf unmarshal does not support stream body".to_string(),
213 )),
214 Body::Empty => Err(CamelError::TypeConversionFailed(
215 "protobuf unmarshal does not support empty body".to_string(),
216 )),
217 Body::Xml(_) => Err(CamelError::TypeConversionFailed(
218 "protobuf unmarshal does not support XML body".to_string(),
219 )),
220 _ => Err(CamelError::TypeConversionFailed(
221 "protobuf unmarshal does not support this body type".to_string(),
222 )),
223 }
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use std::path::PathBuf;
230
231 use bytes::Bytes;
232 use camel_api::body::Body;
233 use camel_api::data_format::DataFormat;
234 use camel_api::error::CamelError;
235 use serde_json::json;
236
237 use super::{ProtobufConfig, ProtobufDataFormat};
238
239 fn test_proto_path() -> PathBuf {
240 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
241 .join("tests")
242 .join("helloworld.proto")
243 }
244
245 fn data_format() -> ProtobufDataFormat {
246 ProtobufDataFormat::new(test_proto_path(), "helloworld.HelloRequest")
247 .expect("should load descriptor")
248 }
249
250 #[test]
251 fn test_name() {
252 let df = data_format();
253 assert_eq!(df.name(), "protobuf");
254 }
255
256 #[test]
257 fn test_marshal_json_to_bytes() {
258 let df = data_format();
259 let body = Body::Json(json!({ "name": "Alice" }));
260 let out = df.marshal(body).expect("marshal should succeed");
261 match out {
262 Body::Bytes(b) => assert!(!b.is_empty()),
263 other => panic!("expected bytes, got {other:?}"),
264 }
265 }
266
267 #[test]
268 fn test_unmarshal_bytes_to_json() {
269 let df = data_format();
270 let bytes = match df
271 .marshal(Body::Json(json!({ "name": "Alice" })))
272 .expect("marshal should succeed")
273 {
274 Body::Bytes(b) => b,
275 other => panic!("expected bytes, got {other:?}"),
276 };
277
278 let out = df
279 .unmarshal(Body::Bytes(bytes))
280 .expect("unmarshal should succeed");
281 match out {
282 Body::Json(v) => assert_eq!(v, json!({ "name": "Alice" })),
283 other => panic!("expected json, got {other:?}"),
284 }
285 }
286
287 #[test]
288 fn test_roundtrip_json_bytes_json() {
289 let df = data_format();
290 let input = json!({ "name": "Bob" });
291 let bytes = match df
292 .marshal(Body::Json(input.clone()))
293 .expect("marshal should succeed")
294 {
295 Body::Bytes(b) => b,
296 other => panic!("expected bytes, got {other:?}"),
297 };
298 let output = match df
299 .unmarshal(Body::Bytes(bytes))
300 .expect("unmarshal should succeed")
301 {
302 Body::Json(v) => v,
303 other => panic!("expected json, got {other:?}"),
304 };
305 assert_eq!(output, input);
306 }
307
308 #[test]
309 fn test_marshal_bytes_passthrough() {
310 let df = data_format();
311 let body = Body::Bytes(Bytes::from_static(b"raw"));
312 let err = df
313 .marshal(body)
314 .expect_err("invalid bytes should be rejected");
315 assert!(
316 err.to_string()
317 .contains("protobuf marshal: invalid bytes for type")
318 );
319 }
320
321 #[test]
322 fn test_marshal_valid_bytes_accepted() {
323 let df = data_format();
324 let bytes = match df
325 .marshal(Body::Json(json!({ "name": "Alice" })))
326 .expect("marshal should succeed")
327 {
328 Body::Bytes(b) => b,
329 other => panic!("expected bytes, got {other:?}"),
330 };
331 let out = df
332 .marshal(Body::Bytes(bytes.clone()))
333 .expect("valid protobuf bytes should be accepted");
334 assert_eq!(out, Body::Bytes(bytes));
335 }
336
337 #[test]
338 fn test_unmarshal_json_passthrough() {
339 let df = data_format();
340 let body = Body::Json(json!({ "name": "Passthrough" }));
341 let out = df
342 .unmarshal(body.clone())
343 .expect("unmarshal should pass through JSON");
344 assert_eq!(out, body);
345 }
346
347 #[test]
348 fn test_marshal_empty_rejected() {
349 let df = data_format();
350 let err = df.marshal(Body::Empty).expect_err("empty must be rejected");
351 assert!(format!("{err}").contains("empty"));
352 }
353
354 #[test]
355 fn test_unmarshal_empty_rejected() {
356 let df = data_format();
357 let err = df
358 .unmarshal(Body::Empty)
359 .expect_err("empty must be rejected");
360 assert!(format!("{err}").contains("empty"));
361 }
362
363 #[test]
364 fn test_message_not_found_error() {
365 let err = ProtobufDataFormat::new(test_proto_path(), "helloworld.DoesNotExist")
366 .err()
367 .expect("unknown message should fail");
368 assert!(matches!(err, CamelError::Config(_)));
369 }
370
371 #[test]
372 fn test_empty_instance_class_rejected() {
373 let config = ProtobufConfig {
374 instance_class: Some("".into()),
375 ..Default::default()
376 };
377 assert!(config.validate().is_err());
378 }
379
380 #[test]
381 fn test_valid_instance_class_accepted() {
382 let config = ProtobufConfig {
383 instance_class: Some("com.example.MyMessage".into()),
384 ..Default::default()
385 };
386 assert!(config.validate().is_ok());
387 }
388
389 #[test]
390 fn test_no_instance_class_valid() {
391 let config = ProtobufConfig::default();
392 assert!(config.validate().is_ok());
393 }
394
395 #[test]
398 fn test_encode_decode_roundtrip() {
399 let df = data_format();
400 let original = json!({ "name": "RoundtripCharlie" });
401
402 let encoded = match df
404 .marshal(Body::Json(original.clone()))
405 .expect("marshal should succeed")
406 {
407 Body::Bytes(b) => b,
408 other => panic!("expected bytes after marshal, got {other:?}"),
409 };
410 assert!(!encoded.is_empty(), "encoded bytes should not be empty");
411
412 let decoded = match df
414 .unmarshal(Body::Bytes(encoded))
415 .expect("unmarshal should succeed")
416 {
417 Body::Json(v) => v,
418 other => panic!("expected json after unmarshal, got {other:?}"),
419 };
420
421 assert_eq!(decoded, original, "roundtrip should preserve field values");
422 }
423
424 #[test]
425 fn test_unmarshal_rejects_oversized_bytes() {
426 let df = data_format().with_max_decode_bytes(16);
427 let body = Body::Bytes(Bytes::from_static(
429 b"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
430 ));
431 let err = df.unmarshal(body).unwrap_err(); assert!(
433 format!("{err}").contains("max_decode_bytes"),
434 "error should mention max_decode_bytes: {err}"
435 );
436 }
437
438 #[test]
439 fn test_unmarshal_default_cap_accepts_valid_bytes() {
440 let df = data_format(); let bytes = match df
442 .marshal(Body::Json(json!({ "name": "Alice" })))
443 .unwrap() {
445 Body::Bytes(b) => b,
446 _ => panic!("expected bytes"),
447 };
448 let out = df.unmarshal(Body::Bytes(bytes)).unwrap(); assert!(matches!(out, Body::Json(_)));
450 }
451
452 #[test]
453 fn test_with_max_decode_bytes_overrides_default() {
454 let df = data_format().with_max_decode_bytes(1);
455 assert_eq!(df.max_decode_bytes(), 1);
456 }
457
458 fn recursive_node_data_format() -> ProtobufDataFormat {
462 ProtobufDataFormat::new(
463 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
464 .join("tests")
465 .join("fixtures")
466 .join("recursive.proto"),
467 "test.Node",
468 )
469 .expect("recursive.proto must compile and expose test.Node") }
471
472 #[test]
473 fn test_unmarshal_recursive_schema_hits_recursion_limit() {
474 let df = recursive_node_data_format();
484 let mut payload: Vec<u8> = Vec::new();
487 for _ in 0..200 {
488 let mut wrapped = vec![0x0a]; let mut n = payload.len() as u64;
490 loop {
491 let mut byte = (n & 0x7f) as u8;
492 n >>= 7;
493 if n != 0 {
494 byte |= 0x80;
495 }
496 wrapped.push(byte);
497 if n == 0 {
498 break;
499 }
500 }
501 wrapped.extend_from_slice(&payload);
502 payload = wrapped;
503 }
504 assert!(
505 payload.len() < df.max_decode_bytes(),
506 "fixture must sit under byte cap"
507 );
508 let err = df
509 .unmarshal(Body::Bytes(Bytes::from(payload)))
510 .expect_err("deeply-nested recursive payload must be rejected"); let msg = format!("{err}").to_lowercase();
512 assert!(
513 msg.contains("recursion") || msg.contains("limit"),
514 "decode must surface prost's recursion-limit error, got: {msg}"
515 );
516 }
517}