1use crate::error::LaserError;
2use crate::query::{SchemaDef, SchemaSource};
3use prost_reflect::{DescriptorPool, DynamicMessage, MessageDescriptor};
4use serde::Serialize;
5use std::sync::Arc;
6
7#[derive(Clone)]
32pub enum CompiledSchema {
33 Avro(apache_avro::Schema),
35 Protobuf(MessageDescriptor),
37 Json(Arc<jsonschema::Validator>),
40}
41
42impl std::fmt::Debug for CompiledSchema {
43 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Avro(schema) => formatter.debug_tuple("Avro").field(schema).finish(),
46 Self::Protobuf(descriptor) => {
47 formatter.debug_tuple("Protobuf").field(descriptor).finish()
48 }
49 Self::Json(_) => formatter.debug_tuple("Json").finish(),
50 }
51 }
52}
53
54impl CompiledSchema {
55 pub fn compile(def: &SchemaDef) -> Result<Self, LaserError> {
60 match &def.source {
61 SchemaSource::Avro { schema } => {
62 let parsed = apache_avro::Schema::parse_str(schema).map_err(|error| {
63 LaserError::Invalid(format!("schema {}: unparseable Avro: {error}", def.id))
64 })?;
65 Ok(Self::Avro(parsed))
66 }
67 SchemaSource::Protobuf {
68 descriptor_set,
69 message_type,
70 } => {
71 let pool = DescriptorPool::decode(descriptor_set.as_slice()).map_err(|error| {
72 LaserError::Invalid(format!(
73 "schema {}: undecodable Protobuf descriptor set: {error}",
74 def.id
75 ))
76 })?;
77 let descriptor = pool.get_message_by_name(message_type).ok_or_else(|| {
78 LaserError::Invalid(format!(
79 "schema {}: descriptor set has no message `{message_type}`",
80 def.id
81 ))
82 })?;
83 Ok(Self::Protobuf(descriptor))
84 }
85 SchemaSource::JsonSchema { schema } => {
86 let value: serde_json::Value = serde_json::from_str(schema).map_err(|error| {
87 LaserError::Invalid(format!(
88 "schema {}: JSON Schema is not valid JSON: {error}",
89 def.id
90 ))
91 })?;
92 let validator = jsonschema::validator_for(&value).map_err(|error| {
93 LaserError::Invalid(format!(
94 "schema {}: JSON Schema does not compile: {error}",
95 def.id
96 ))
97 })?;
98 Ok(Self::Json(Arc::new(validator)))
99 }
100 _ => Err(LaserError::Invalid(format!(
101 "schema {}: unknown schema source kind",
102 def.id
103 ))),
104 }
105 }
106
107 pub fn validate(&self, payload: &[u8]) -> bool {
114 self.decode(payload).is_ok()
115 }
116
117 pub fn validate_value(&self, value: &serde_json::Value) -> bool {
122 match self {
123 Self::Json(validator) => validator.is_valid(value),
124 Self::Avro(_) | Self::Protobuf(_) => false,
125 }
126 }
127
128 pub fn decode(&self, payload: &[u8]) -> Result<serde_json::Value, LaserError> {
132 match self {
133 Self::Avro(schema) => {
134 let mut cursor = payload;
135 let value = apache_avro::reader::datum::GenericDatumReader::builder(schema)
136 .build()
137 .and_then(|reader| reader.read_value(&mut cursor))
138 .map_err(|error| {
139 LaserError::Codec(format!("payload does not decode as Avro: {error}"))
140 })?;
141 apache_avro::from_value::<serde_json::Value>(&bytes_as_arrays(value)).map_err(
142 |error| {
143 LaserError::Codec(format!("Avro value does not lower to JSON: {error}"))
144 },
145 )
146 }
147 Self::Protobuf(descriptor) => {
148 let message =
149 DynamicMessage::decode(descriptor.clone(), payload).map_err(|error| {
150 LaserError::Codec(format!("payload does not decode as Protobuf: {error}"))
151 })?;
152 serde_json::to_value(&message).map_err(|error| {
153 LaserError::Codec(format!("Protobuf message does not lower to JSON: {error}"))
154 })
155 }
156 Self::Json(validator) => {
157 let value: serde_json::Value =
158 serde_json::from_slice(payload).map_err(|error| {
159 LaserError::Codec(format!("payload does not parse as JSON: {error}"))
160 })?;
161 if !validator.is_valid(&value) {
162 return Err(LaserError::Codec(
163 "payload fails its JSON Schema".to_owned(),
164 ));
165 }
166 Ok(value)
167 }
168 }
169 }
170
171 pub fn encode_avro<T: Serialize>(&self, body: &T) -> Result<Vec<u8>, LaserError> {
177 let Self::Avro(schema) = self else {
178 return Err(LaserError::Invalid(
179 "encode_avro requires an Avro schema".to_owned(),
180 ));
181 };
182 let json = serde_json::to_value(body)
187 .map_err(|error| LaserError::Codec(format!("body does not lower to JSON: {error}")))?;
188 let resolved = apache_avro::types::Value::try_from(json)
189 .and_then(|value| arrays_as_bytes(value, schema).resolve(schema))
190 .map_err(|error| {
191 LaserError::Codec(format!("body does not match the Avro schema: {error}"))
192 })?;
193 apache_avro::writer::datum::GenericDatumWriter::builder(schema)
194 .build()
195 .and_then(|writer| writer.write_value_to_vec(resolved))
196 .map_err(|error| LaserError::Codec(format!("Avro datum encode failed: {error}")))
197 }
198}
199
200fn arrays_as_bytes(
204 value: apache_avro::types::Value,
205 schema: &apache_avro::Schema,
206) -> apache_avro::types::Value {
207 use apache_avro::Schema;
208 use apache_avro::types::Value;
209 match (schema, value) {
210 (Schema::Bytes | Schema::Fixed(_), Value::Array(items)) => {
211 let bytes: Option<Vec<u8>> = items
212 .iter()
213 .map(|item| match item {
214 Value::Int(byte) => u8::try_from(*byte).ok(),
215 Value::Long(byte) => u8::try_from(*byte).ok(),
216 _ => None,
217 })
218 .collect();
219 bytes.map_or(Value::Array(items), Value::Bytes)
220 }
221 (Schema::Array(array), Value::Array(items)) => Value::Array(
222 items
223 .into_iter()
224 .map(|item| arrays_as_bytes(item, &array.items))
225 .collect(),
226 ),
227 (Schema::Map(map), Value::Map(entries)) => Value::Map(
228 entries
229 .into_iter()
230 .map(|(key, item)| (key, arrays_as_bytes(item, &map.types)))
231 .collect(),
232 ),
233 (Schema::Record(record), Value::Map(entries)) => Value::Map(
234 entries
235 .into_iter()
236 .map(|(key, item)| {
237 let item = match record.fields.iter().find(|field| field.name == key) {
238 Some(field) => arrays_as_bytes(item, &field.schema),
239 None => item,
240 };
241 (key, item)
242 })
243 .collect(),
244 ),
245 (Schema::Union(union), value) => {
246 let variant = union.variants().iter().find(|variant| {
247 matches!(
248 (variant, &value),
249 (Schema::Bytes | Schema::Fixed(_), Value::Array(_))
250 | (Schema::Record(_) | Schema::Map(_), Value::Map(_))
251 )
252 });
253 let has_array = union
254 .variants()
255 .iter()
256 .any(|variant| matches!(variant, Schema::Array(_)));
257 match variant {
258 Some(variant) if !(has_array && matches!(value, Value::Array(_))) => {
259 arrays_as_bytes(value, variant)
260 }
261 _ => value,
262 }
263 }
264 (_, value) => value,
265 }
266}
267
268fn bytes_as_arrays(value: apache_avro::types::Value) -> apache_avro::types::Value {
271 use apache_avro::types::Value;
272 match value {
273 Value::Bytes(bytes) | Value::Fixed(_, bytes) => Value::Array(
274 bytes
275 .into_iter()
276 .map(|byte| Value::Int(i32::from(byte)))
277 .collect(),
278 ),
279 Value::Array(items) => Value::Array(items.into_iter().map(bytes_as_arrays).collect()),
280 Value::Map(entries) => Value::Map(
281 entries
282 .into_iter()
283 .map(|(key, item)| (key, bytes_as_arrays(item)))
284 .collect(),
285 ),
286 Value::Record(fields) => Value::Record(
287 fields
288 .into_iter()
289 .map(|(name, item)| (name, bytes_as_arrays(item)))
290 .collect(),
291 ),
292 Value::Union(index, item) => Value::Union(index, Box::new(bytes_as_arrays(*item))),
293 other => other,
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn given_bytes_and_fixed_fields_when_encoded_then_should_decode_back() {
303 let def = SchemaDef {
304 id: 8,
305 source: SchemaSource::Avro {
306 schema: r#"{
307 "type":"record","name":"Frame",
308 "fields":[
309 {"name":"body","type":"bytes"},
310 {"name":"digest","type":{"type":"fixed","name":"Digest","size":2}},
311 {"name":"tag","type":["null","bytes"]}
312 ]
313 }"#
314 .to_owned(),
315 },
316 name: None,
317 version: None,
318 };
319 let compiled = CompiledSchema::compile(&def).expect("schema compiles");
320 let body = serde_json::json!({"body": [1, 2, 255], "digest": [7, 9], "tag": [4]});
321 let datum = compiled.encode_avro(&body).expect("body encodes");
322 assert!(compiled.validate(&datum), "own encoding validates");
323 assert_eq!(compiled.decode(&datum).expect("datum decodes"), body);
324 }
325
326 const READING_AVRO_SCHEMA: &str = r#"{
327 "type":"record","name":"Reading",
328 "fields":[
329 {"name":"host","type":"string"},
330 {"name":"cpu","type":"long"}
331 ]
332 }"#;
333
334 #[derive(Serialize)]
335 struct Reading {
336 host: String,
337 cpu: i64,
338 }
339
340 fn avro_def() -> SchemaDef {
341 SchemaDef {
342 id: 7,
343 source: SchemaSource::Avro {
344 schema: READING_AVRO_SCHEMA.to_owned(),
345 },
346 name: None,
347 version: None,
348 }
349 }
350
351 #[test]
352 fn given_an_avro_def_when_encoded_then_should_decode_and_validate_back() {
353 let compiled = CompiledSchema::compile(&avro_def()).expect("schema compiles");
354 let reading = Reading {
355 host: "node-7".to_owned(),
356 cpu: 42,
357 };
358 let datum = compiled.encode_avro(&reading).expect("body encodes");
359 assert!(compiled.validate(&datum), "own encoding validates");
360 let value = compiled.decode(&datum).expect("datum decodes");
361 assert_eq!(
362 value.pointer("/host").and_then(|v| v.as_str()),
363 Some("node-7")
364 );
365 assert_eq!(value.pointer("/cpu").and_then(|v| v.as_i64()), Some(42));
366 }
367
368 #[test]
369 fn given_a_mismatched_body_when_encoded_then_should_error() {
370 #[derive(Serialize)]
371 struct Wrong {
372 unrelated: bool,
373 }
374 let compiled = CompiledSchema::compile(&avro_def()).expect("schema compiles");
375 let result = compiled.encode_avro(&Wrong { unrelated: true });
376 assert!(matches!(result, Err(LaserError::Codec(_))));
377 }
378
379 #[test]
380 fn given_garbage_bytes_when_validated_then_should_be_false() {
381 let compiled = CompiledSchema::compile(&avro_def()).expect("schema compiles");
382 assert!(!compiled.validate(b""));
383 }
384
385 #[test]
386 fn given_an_unparseable_avro_def_when_compiled_then_should_be_invalid() {
387 let def = SchemaDef {
388 id: 9,
389 source: SchemaSource::Avro {
390 schema: "{not valid".to_owned(),
391 },
392 name: None,
393 version: None,
394 };
395 assert!(matches!(
396 CompiledSchema::compile(&def),
397 Err(LaserError::Invalid(_))
398 ));
399 }
400
401 #[test]
402 fn given_a_json_schema_def_when_compiled_then_should_validate_and_decode_json() {
403 let def = SchemaDef {
404 id: 5,
405 source: SchemaSource::JsonSchema {
406 schema: r#"{
407 "type":"object",
408 "required":["host","cpu"],
409 "properties":{
410 "host":{"type":"string"},
411 "cpu":{"type":"integer","minimum":0}
412 }
413 }"#
414 .to_owned(),
415 },
416 name: None,
417 version: None,
418 };
419 let compiled = CompiledSchema::compile(&def).expect("schema compiles");
420 assert!(compiled.validate(br#"{"host":"node-7","cpu":42}"#));
421 assert!(!compiled.validate(br#"{"host":"node-7","cpu":"42"}"#));
422 assert!(!compiled.validate(b"not json"));
423 assert!(compiled.validate_value(&serde_json::json!({"host":"a","cpu":1})));
424 assert!(!compiled.validate_value(&serde_json::json!({"cpu":1})));
425 let value = compiled
426 .decode(br#"{"host":"node-7","cpu":42}"#)
427 .expect("valid payload decodes");
428 assert_eq!(
429 value.pointer("/host").and_then(|v| v.as_str()),
430 Some("node-7")
431 );
432 assert!(matches!(
433 compiled.decode(br#"{"cpu":42}"#),
434 Err(LaserError::Codec(_))
435 ));
436 assert!(matches!(
437 compiled.encode_avro(&serde_json::json!({})),
438 Err(LaserError::Invalid(_))
439 ));
440 }
441
442 #[test]
443 fn given_an_uncompilable_json_schema_def_when_compiled_then_should_be_invalid() {
444 let def = SchemaDef {
445 id: 6,
446 source: SchemaSource::JsonSchema {
447 schema: "{not json".to_owned(),
448 },
449 name: None,
450 version: None,
451 };
452 assert!(matches!(
453 CompiledSchema::compile(&def),
454 Err(LaserError::Invalid(_))
455 ));
456 }
457
458 #[test]
459 fn given_an_avro_def_when_value_validated_then_should_report_family_mismatch() {
460 let compiled = CompiledSchema::compile(&avro_def()).expect("schema compiles");
461 assert!(!compiled.validate_value(&serde_json::json!({"host":"a","cpu":1})));
462 }
463
464 #[test]
465 fn given_a_protobuf_def_when_compiled_then_should_validate_real_messages() {
466 use prost::Message;
467 let dir = tempfile::tempdir().expect("temp dir");
468 let proto = dir.path().join("reading.proto");
469 std::fs::write(
470 &proto,
471 "syntax = \"proto3\";\npackage fleet;\nmessage Reading { string host = 1; int64 cpu = 2; }\n",
472 )
473 .expect("proto written");
474 let descriptor_set = protox::compile([&proto], [dir.path()])
475 .expect("proto compiles")
476 .encode_to_vec();
477 let def = SchemaDef {
478 id: 3,
479 source: SchemaSource::Protobuf {
480 descriptor_set: descriptor_set.clone(),
481 message_type: "fleet.Reading".to_owned(),
482 },
483 name: None,
484 version: None,
485 };
486 let compiled = CompiledSchema::compile(&def).expect("schema compiles");
487
488 let pool = DescriptorPool::decode(descriptor_set.as_slice()).expect("pool decodes");
489 let descriptor = pool.get_message_by_name("fleet.Reading").expect("message");
490 let mut message = DynamicMessage::new(descriptor);
491 message.set_field_by_name("host", prost_reflect::Value::String("node-7".into()));
492 message.set_field_by_name("cpu", prost_reflect::Value::I64(42));
493 let payload = message.encode_to_vec();
494
495 assert!(compiled.validate(&payload));
496 assert!(!compiled.validate(b"\xff\xff\xff"));
497 let value = compiled.decode(&payload).expect("decodes");
498 assert_eq!(
499 value.pointer("/host").and_then(|v| v.as_str()),
500 Some("node-7")
501 );
502 let encode = compiled.encode_avro(&serde_json::json!({}));
503 assert!(matches!(encode, Err(LaserError::Invalid(_))));
504 }
505}