faucet_source_kafka/
decode.rs1use base64::Engine;
5use faucet_common_kafka::KafkaValueFormat;
6use faucet_core::FaucetError;
7use serde_json::Value;
8
9#[cfg(feature = "schema-registry")]
10use faucet_common_kafka::schema_registry::{
11 avro, client::SchemaRegistryClient, json_schema, protobuf,
12};
13
14pub async fn decode(
16 bytes: Option<&[u8]>,
17 format: &KafkaValueFormat,
18 #[cfg(feature = "schema-registry")] sr_client: Option<&SchemaRegistryClient>,
19) -> Result<Value, FaucetError> {
20 let Some(bytes) = bytes else {
21 return Ok(Value::Null);
22 };
23 match format {
24 KafkaValueFormat::Json => serde_json::from_slice(bytes)
25 .map_err(|e| FaucetError::Source(format!("kafka json decode: {e}"))),
26 KafkaValueFormat::RawString => {
27 let s = std::str::from_utf8(bytes)
28 .map_err(|e| FaucetError::Source(format!("kafka raw_string decode: {e}")))?;
29 Ok(Value::String(s.to_string()))
30 }
31 KafkaValueFormat::Bytes => {
32 let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
33 Ok(Value::String(encoded))
34 }
35 #[cfg(feature = "schema-registry")]
36 KafkaValueFormat::ConfluentAvro { .. } => {
37 let client = sr_client.ok_or_else(|| {
38 FaucetError::Config(
39 "ConfluentAvro selected but no SchemaRegistryClient available".into(),
40 )
41 })?;
42 avro::decode(client, bytes).await
43 }
44 #[cfg(feature = "schema-registry")]
45 KafkaValueFormat::ConfluentProtobuf { .. } => {
46 let client = sr_client.ok_or_else(|| {
47 FaucetError::Config(
48 "ConfluentProtobuf selected but no SchemaRegistryClient available".into(),
49 )
50 })?;
51 protobuf::decode(client, bytes).await
52 }
53 #[cfg(feature = "schema-registry")]
54 KafkaValueFormat::ConfluentJsonSchema { validate, .. } => {
55 let client = sr_client.ok_or_else(|| {
56 FaucetError::Config(
57 "ConfluentJsonSchema selected but no SchemaRegistryClient available".into(),
58 )
59 })?;
60 json_schema::decode(client, bytes, *validate).await
61 }
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[tokio::test]
70 async fn decode_json_object() {
71 let bytes = br#"{"id": 7, "name": "x"}"#;
72 let v = decode(
73 Some(bytes),
74 &KafkaValueFormat::Json,
75 #[cfg(feature = "schema-registry")]
76 None,
77 )
78 .await
79 .unwrap();
80 assert_eq!(v["id"], 7);
81 }
82
83 #[tokio::test]
84 async fn decode_raw_string_utf8() {
85 let bytes = "hello".as_bytes();
86 let v = decode(
87 Some(bytes),
88 &KafkaValueFormat::RawString,
89 #[cfg(feature = "schema-registry")]
90 None,
91 )
92 .await
93 .unwrap();
94 assert_eq!(v, Value::String("hello".into()));
95 }
96
97 #[tokio::test]
98 async fn decode_bytes_base64_encodes() {
99 let bytes = &[0xDE, 0xAD, 0xBE, 0xEF];
100 let v = decode(
101 Some(bytes),
102 &KafkaValueFormat::Bytes,
103 #[cfg(feature = "schema-registry")]
104 None,
105 )
106 .await
107 .unwrap();
108 assert_eq!(v, Value::String("3q2+7w==".into()));
109 }
110
111 #[tokio::test]
112 async fn decode_none_returns_null() {
113 let v = decode(
114 None,
115 &KafkaValueFormat::Json,
116 #[cfg(feature = "schema-registry")]
117 None,
118 )
119 .await
120 .unwrap();
121 assert_eq!(v, Value::Null);
122 }
123
124 #[tokio::test]
125 async fn decode_invalid_json_errors() {
126 let bytes = b"{not json";
127 let err = decode(
128 Some(bytes),
129 &KafkaValueFormat::Json,
130 #[cfg(feature = "schema-registry")]
131 None,
132 )
133 .await
134 .unwrap_err();
135 assert!(format!("{err}").contains("json"));
136 }
137
138 #[tokio::test]
139 async fn decode_raw_string_rejects_invalid_utf8() {
140 let bytes = &[0xFF, 0xFE];
141 let err = decode(
142 Some(bytes),
143 &KafkaValueFormat::RawString,
144 #[cfg(feature = "schema-registry")]
145 None,
146 )
147 .await
148 .unwrap_err();
149 let m = format!("{err}").to_lowercase();
150 assert!(m.contains("utf-8") || m.contains("utf"));
151 }
152
153 #[cfg(feature = "schema-registry")]
154 #[tokio::test]
155 async fn decode_confluent_avro_without_client_is_config_error() {
156 use faucet_common_kafka::SchemaRegistryConfig;
157 let format = KafkaValueFormat::ConfluentAvro {
158 schema_registry: SchemaRegistryConfig::new("http://localhost:8081"),
159 };
160 let err = decode(Some(b"\x00\x00\x00\x00\x01"), &format, None)
161 .await
162 .unwrap_err();
163 match err {
164 FaucetError::Config(msg) => assert!(
165 msg.contains("ConfluentAvro") && msg.contains("no SchemaRegistryClient"),
166 "unexpected message: {msg}"
167 ),
168 other => panic!("expected Config error, got {other:?}"),
169 }
170 }
171
172 #[cfg(feature = "schema-registry")]
173 #[tokio::test]
174 async fn decode_confluent_protobuf_without_client_is_config_error() {
175 use faucet_common_kafka::SchemaRegistryConfig;
176 let format = KafkaValueFormat::ConfluentProtobuf {
177 schema_registry: SchemaRegistryConfig::new("http://localhost:8081"),
178 };
179 let err = decode(Some(b"\x00\x00\x00\x00\x01"), &format, None)
180 .await
181 .unwrap_err();
182 match err {
183 FaucetError::Config(msg) => assert!(
184 msg.contains("ConfluentProtobuf") && msg.contains("no SchemaRegistryClient"),
185 "unexpected message: {msg}"
186 ),
187 other => panic!("expected Config error, got {other:?}"),
188 }
189 }
190
191 #[cfg(feature = "schema-registry")]
192 #[tokio::test]
193 async fn decode_confluent_json_schema_without_client_is_config_error() {
194 use faucet_common_kafka::SchemaRegistryConfig;
195 let format = KafkaValueFormat::ConfluentJsonSchema {
196 schema_registry: SchemaRegistryConfig::new("http://localhost:8081"),
197 validate: false,
198 };
199 let err = decode(Some(b"\x00\x00\x00\x00\x01"), &format, None)
200 .await
201 .unwrap_err();
202 match err {
203 FaucetError::Config(msg) => assert!(
204 msg.contains("ConfluentJsonSchema") && msg.contains("no SchemaRegistryClient"),
205 "unexpected message: {msg}"
206 ),
207 other => panic!("expected Config error, got {other:?}"),
208 }
209 }
210}