1use bytes::Bytes;
2use krabka_schema_serde::{RegistryClient, SchemaKind, wire::encode_protobuf};
3use prost::Message as _;
4use prost_reflect::{
5 DescriptorPool, DynamicMessage, MessageDescriptor, Value,
6 prost_types::{
7 DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet,
8 field_descriptor_proto::{Label, Type},
9 },
10};
11
12use crate::{
13 PostgresConnectError,
14 model::{ColumnValue, EntityDifference, EntityKey, Operation, ScalarValue},
15};
16
17const KEY_MESSAGE_INDEX: &[i32] = &[1];
18const VALUE_MESSAGE_INDEX: &[i32] = &[2];
19const PACKAGE: &str = "krabka.connect.postgres";
20const COLUMN_VALUE: &str = "ColumnValue";
21const ENTITY_KEY: &str = "EntityKey";
22const ENTITY_DIFFERENCE: &str = "EntityDifference";
23const KEY_SUBJECT: &str = "krabka-connect-postgres-key";
24const VALUE_SUBJECT: &str = "krabka-connect-postgres-value";
25const KEY_MESSAGE_TYPE: &str = "krabka.connect.postgres.EntityKey";
26const VALUE_MESSAGE_TYPE: &str = "krabka.connect.postgres.EntityDifference";
27const PROTO_SCHEMA: &str = r#"syntax = "proto3";
28package krabka.connect.postgres;
29
30message ColumnValue {
31 string name = 1;
32 string kind = 2;
33 string string_value = 3;
34 bool bool_value = 4;
35 int64 int_value = 5;
36 bytes bytes_value = 6;
37 bool is_null = 7;
38}
39
40message EntityKey {
41 string table = 1;
42 repeated ColumnValue columns = 2;
43}
44
45message EntityDifference {
46 string table = 1;
47 string operation = 2;
48 string lsn = 3;
49 repeated ColumnValue before = 4;
50 repeated ColumnValue after = 5;
51 EntityKey key = 6;
52 int64 txid = 7;
53 int64 commit_timestamp_ms = 8;
54}
55"#;
56
57#[derive(Debug, Clone)]
58pub struct PostgresProtoEncoder {
59 key_schema_id: u32,
60 value_schema_id: u32,
61 key: MessageDescriptor,
62 value: MessageDescriptor,
63 column_value: MessageDescriptor,
64}
65
66impl PostgresProtoEncoder {
67 pub async fn from_registry(registry_url: &str) -> Result<Self, PostgresConnectError> {
72 let registry = RegistryClient::new(registry_url);
73 let key_schema_id = registry
74 .register(
75 KEY_SUBJECT,
76 SchemaKind::Protobuf,
77 PROTO_SCHEMA,
78 &[],
79 Some(KEY_MESSAGE_TYPE),
80 )
81 .await
82 .map_err(registry_error)?;
83 let value_schema_id = registry
84 .register(
85 VALUE_SUBJECT,
86 SchemaKind::Protobuf,
87 PROTO_SCHEMA,
88 &[],
89 Some(VALUE_MESSAGE_TYPE),
90 )
91 .await
92 .map_err(registry_error)?;
93 Self::with_schema_ids(key_schema_id, value_schema_id)
94 }
95
96 #[cfg(test)]
97 pub(crate) fn for_test() -> Result<Self, PostgresConnectError> {
98 Self::with_schema_ids(41, 57)
99 }
100
101 fn with_schema_ids(
104 key_schema_id: u32,
105 value_schema_id: u32,
106 ) -> Result<Self, PostgresConnectError> {
107 let pool = DescriptorPool::from_file_descriptor_set(schema_descriptor_set())
108 .map_err(convert_error)?;
109
110 Ok(Self {
111 key_schema_id,
112 value_schema_id,
113 key: message_descriptor(&pool, ENTITY_KEY)?,
114 value: message_descriptor(&pool, ENTITY_DIFFERENCE)?,
115 column_value: message_descriptor(&pool, COLUMN_VALUE)?,
116 })
117 }
118
119 pub fn encode_key(&self, key: &EntityKey) -> Result<Bytes, PostgresConnectError> {
122 let message = self.key_to_message(key)?;
123 Ok(encode_protobuf(
124 self.key_schema_id,
125 KEY_MESSAGE_INDEX,
126 &message.encode_to_vec(),
127 ))
128 }
129
130 pub fn encode_value(&self, value: &EntityDifference) -> Result<Bytes, PostgresConnectError> {
133 let message = self.difference_to_message(value)?;
134 Ok(encode_protobuf(
135 self.value_schema_id,
136 VALUE_MESSAGE_INDEX,
137 &message.encode_to_vec(),
138 ))
139 }
140
141 fn key_to_message(&self, key: &EntityKey) -> Result<DynamicMessage, PostgresConnectError> {
142 let mut message = DynamicMessage::new(self.key.clone());
143 set_field(&mut message, "table", Value::String(key.table.clone()))?;
144 set_field(
145 &mut message,
146 "columns",
147 Value::List(self.columns_to_values(&key.columns)?),
148 )?;
149 Ok(message)
150 }
151
152 fn difference_to_message(
153 &self,
154 difference: &EntityDifference,
155 ) -> Result<DynamicMessage, PostgresConnectError> {
156 let mut message = DynamicMessage::new(self.value.clone());
157 set_field(
158 &mut message,
159 "table",
160 Value::String(difference.table.clone()),
161 )?;
162 set_field(
163 &mut message,
164 "operation",
165 Value::String(operation_name(difference.op).to_owned()),
166 )?;
167 set_field(
168 &mut message,
169 "lsn",
170 Value::String(difference.lsn.to_string()),
171 )?;
172 set_field(
173 &mut message,
174 "before",
175 Value::List(self.columns_to_values(&difference.before)?),
176 )?;
177 set_field(
178 &mut message,
179 "after",
180 Value::List(self.columns_to_values(&difference.after)?),
181 )?;
182 set_field(
183 &mut message,
184 "key",
185 Value::Message(self.key_to_message(&difference.key)?),
186 )?;
187 if let Some(txid) = difference.txid {
188 set_field(&mut message, "txid", Value::I64(txid.0))?;
189 }
190 if let Some(commit_timestamp_ms) = difference.commit_timestamp_ms {
191 set_field(
192 &mut message,
193 "commit_timestamp_ms",
194 Value::I64(commit_timestamp_ms),
195 )?;
196 }
197 Ok(message)
198 }
199
200 fn columns_to_values(
201 &self,
202 columns: &[ColumnValue],
203 ) -> Result<Vec<Value>, PostgresConnectError> {
204 columns
205 .iter()
206 .map(|column| self.column_to_value(column).map(Value::Message))
207 .collect()
208 }
209
210 fn column_to_value(
211 &self,
212 column: &ColumnValue,
213 ) -> Result<DynamicMessage, PostgresConnectError> {
214 let mut message = DynamicMessage::new(self.column_value.clone());
215 set_field(&mut message, "name", Value::String(column.name.clone()))?;
216 set_scalar_fields(&mut message, &column.value)?;
217 Ok(message)
218 }
219}
220
221fn schema_descriptor_set() -> FileDescriptorSet {
222 FileDescriptorSet {
223 file: vec![FileDescriptorProto {
224 name: Some("krabka/connect/postgres/cdc.proto".to_owned()),
225 package: Some(PACKAGE.to_owned()),
226 syntax: Some("proto3".to_owned()),
227 message_type: vec![
228 DescriptorProto {
229 name: Some(COLUMN_VALUE.to_owned()),
230 field: vec![
231 field("name", 1, Type::String),
232 field("kind", 2, Type::String),
233 field("string_value", 3, Type::String),
234 field("bool_value", 4, Type::Bool),
235 field("int_value", 5, Type::Int64),
236 field("bytes_value", 6, Type::Bytes),
237 field("is_null", 7, Type::Bool),
238 ],
239 ..DescriptorProto::default()
240 },
241 DescriptorProto {
242 name: Some(ENTITY_KEY.to_owned()),
243 field: vec![
244 field("table", 1, Type::String),
245 repeated_message_field("columns", 2, COLUMN_VALUE),
246 ],
247 ..DescriptorProto::default()
248 },
249 DescriptorProto {
250 name: Some(ENTITY_DIFFERENCE.to_owned()),
251 field: vec![
252 field("table", 1, Type::String),
253 field("operation", 2, Type::String),
254 field("lsn", 3, Type::String),
255 repeated_message_field("before", 4, COLUMN_VALUE),
256 repeated_message_field("after", 5, COLUMN_VALUE),
257 message_field("key", 6, ENTITY_KEY),
258 field("txid", 7, Type::Int64),
259 field("commit_timestamp_ms", 8, Type::Int64),
260 ],
261 ..DescriptorProto::default()
262 },
263 ],
264 ..FileDescriptorProto::default()
265 }],
266 }
267}
268
269fn field(name: &str, number: i32, field_type: Type) -> FieldDescriptorProto {
270 FieldDescriptorProto {
271 name: Some(name.to_owned()),
272 number: Some(number),
273 label: Some(Label::Optional as i32),
274 r#type: Some(field_type as i32),
275 ..FieldDescriptorProto::default()
276 }
277}
278
279fn message_field(name: &str, number: i32, message_name: &str) -> FieldDescriptorProto {
280 FieldDescriptorProto {
281 type_name: Some(full_type_name(message_name)),
282 ..field(name, number, Type::Message)
283 }
284}
285
286fn repeated_message_field(name: &str, number: i32, message_name: &str) -> FieldDescriptorProto {
287 FieldDescriptorProto {
288 label: Some(Label::Repeated as i32),
289 ..message_field(name, number, message_name)
290 }
291}
292
293fn full_type_name(message_name: &str) -> String {
294 format!(".{PACKAGE}.{message_name}")
295}
296
297fn message_descriptor(
298 pool: &DescriptorPool,
299 name: &str,
300) -> Result<MessageDescriptor, PostgresConnectError> {
301 pool.get_message_by_name(&format!("{PACKAGE}.{name}"))
302 .ok_or_else(|| PostgresConnectError::Convert(format!("protobuf message {name} not found")))
303}
304
305fn set_field(
306 message: &mut DynamicMessage,
307 name: &str,
308 value: Value,
309) -> Result<(), PostgresConnectError> {
310 message
311 .try_set_field_by_name(name, value)
312 .map_err(convert_error)
313}
314
315fn operation_name(operation: Operation) -> &'static str {
316 match operation {
317 Operation::Insert => "insert",
318 Operation::Update => "update",
319 Operation::Delete => "delete",
320 }
321}
322
323fn set_scalar_fields(
324 message: &mut DynamicMessage,
325 value: &ScalarValue,
326) -> Result<(), PostgresConnectError> {
327 match value {
328 ScalarValue::Null => {
329 set_field(message, "kind", Value::String("null".to_owned()))?;
330 set_field(message, "is_null", Value::Bool(true))?;
331 }
332 ScalarValue::UnchangedToast => {
333 set_field(message, "kind", Value::String("unchanged_toast".to_owned()))?;
334 }
335 ScalarValue::Bool(value) => {
336 set_field(message, "kind", Value::String("bool".to_owned()))?;
337 set_field(message, "bool_value", Value::Bool(*value))?;
338 }
339 ScalarValue::Int(value) => {
340 set_field(message, "kind", Value::String("int".to_owned()))?;
341 set_field(message, "int_value", Value::I64(*value))?;
342 }
343 ScalarValue::Float(value) => {
344 set_field(message, "kind", Value::String("float".to_owned()))?;
345 set_field(message, "string_value", Value::String(value.clone()))?;
346 }
347 ScalarValue::Text(value) => {
348 set_field(message, "kind", Value::String("text".to_owned()))?;
349 set_field(message, "string_value", Value::String(value.clone()))?;
350 }
351 ScalarValue::Bytes(value) => {
352 set_field(message, "kind", Value::String("bytes".to_owned()))?;
353 set_field(
354 message,
355 "bytes_value",
356 Value::Bytes(Bytes::copy_from_slice(value)),
357 )?;
358 }
359 }
360
361 Ok(())
362}
363
364fn convert_error(error: impl std::fmt::Display) -> PostgresConnectError {
365 PostgresConnectError::Convert(error.to_string())
366}
367
368fn registry_error(error: impl std::fmt::Display) -> PostgresConnectError {
369 PostgresConnectError::Convert(format!("schema registry: {error}"))
370}
371
372#[cfg(test)]
373mod tests {
374 use bytes::Bytes;
375 use krabka_schema_serde::wire::decode_protobuf;
376 use prost_reflect::{DescriptorPool, DynamicMessage, Value};
377 use wiremock::{
378 Mock, MockServer, ResponseTemplate,
379 matchers::{body_json, method, path},
380 };
381
382 use super::{
383 COLUMN_VALUE, ENTITY_DIFFERENCE, ENTITY_KEY, KEY_MESSAGE_TYPE, PROTO_SCHEMA,
384 PostgresProtoEncoder, VALUE_MESSAGE_TYPE, message_descriptor, schema_descriptor_set,
385 };
386 use crate::{
387 ColumnValue, EntityDifference, EntityKey, Operation, PgLsn, TableSchema,
388 ids::{RelationId, TransactionId},
389 model::{ColumnSchema, ScalarValue},
390 pgoutput::{RelationCache, RelationEvent, RowEvent, RowEventKind},
391 };
392
393 #[test]
394 fn encoder_frames_key_and_value_as_protobuf() {
395 let encoder =
396 PostgresProtoEncoder::with_schema_ids(41, 57).expect("encoder builds descriptors");
397 let diff = sample_difference();
398 let pool = DescriptorPool::from_file_descriptor_set(schema_descriptor_set())
399 .expect("descriptor pool builds");
400
401 let key = encoder.encode_key(&diff.key).expect("key encodes");
402 let value = encoder.encode_value(&diff).expect("value encodes");
403
404 let (key_id, key_index, key_body) = decode_protobuf(&key).expect("key frame decodes");
405 let key_frame = (key_id, key_index, key_body.is_empty());
406
407 let (value_id, value_index, value_body) =
408 decode_protobuf(&value).expect("value frame decodes");
409 let value_frame = (value_id, value_index, value_body.is_empty());
410
411 let key_message = DynamicMessage::decode(
412 message_descriptor(&pool, ENTITY_KEY).expect("key descriptor"),
413 key_body,
414 )
415 .expect("key body decodes");
416 let key_columns = list_field(&key_message, "columns");
417 let id_column = message_value(&key_columns[0]);
418 let key_projection = (
419 string_field(&key_message, "table"),
420 string_field(id_column, "name"),
421 string_field(id_column, "kind"),
422 i64_field(id_column, "int_value"),
423 );
424
425 let value_message = DynamicMessage::decode(
426 message_descriptor(&pool, ENTITY_DIFFERENCE).expect("value descriptor"),
427 value_body,
428 )
429 .expect("value body decodes");
430 let value_projection = (
431 string_field(&value_message, "table"),
432 string_field(&value_message, "operation"),
433 string_field(&value_message, "lsn"),
434 );
435
436 let after = list_field(&value_message, "after");
437 let name_column = message_value(&after[0]);
438 let name_projection = (
439 string_field(name_column, "name"),
440 string_field(name_column, "kind"),
441 string_field(name_column, "string_value"),
442 );
443
444 let before = list_field(&value_message, "before");
445 let null_column = message_value(&before[1]);
446 let null_projection = (
447 string_field(null_column, "name"),
448 string_field(null_column, "kind"),
449 bool_field(null_column, "is_null"),
450 );
451
452 let avatar_column = message_value(&after[1]);
453 let avatar_projection = (
454 string_field(avatar_column, "name"),
455 string_field(avatar_column, "kind"),
456 bytes_field(avatar_column, "bytes_value"),
457 );
458
459 let unchanged_toast_column = message_value(&after[2]);
460 let unchanged_projection = (
461 string_field(unchanged_toast_column, "name"),
462 string_field(unchanged_toast_column, "kind"),
463 );
464
465 assert2::assert!(key_frame == (41, vec![1], false));
466 assert2::assert!(value_frame == (57, vec![2], false));
467 assert2::assert!(
468 key_projection
469 == (
470 "public.accounts".to_string(),
471 "id".to_string(),
472 "int".to_string(),
473 42,
474 )
475 );
476 assert2::assert!(
477 value_projection
478 == (
479 "public.accounts".to_string(),
480 "update".to_string(),
481 "0/2A".to_string(),
482 )
483 );
484 assert2::assert!(
485 name_projection == ("name".to_string(), "text".to_string(), "new".to_string())
486 );
487 assert2::assert!(null_projection == ("nickname".to_string(), "null".to_string(), true));
488 assert2::assert!(
489 avatar_projection
490 == (
491 "avatar".to_string(),
492 "bytes".to_string(),
493 Bytes::from_static(b"abc"),
494 )
495 );
496 assert2::assert!(
497 unchanged_projection == ("details".to_string(), "unchanged_toast".to_string())
498 );
499 }
500
501 #[tokio::test]
502 async fn registry_allocates_ids_used_by_key_and_value_frames() {
503 let server = MockServer::start().await;
504 for (subject, message_type, id) in [
505 ("krabka-connect-postgres-key", KEY_MESSAGE_TYPE, 73),
506 ("krabka-connect-postgres-value", VALUE_MESSAGE_TYPE, 91),
507 ] {
508 Mock::given(method("POST"))
509 .and(path(format!("/subjects/{subject}/versions")))
510 .and(body_json(serde_json::json!({
511 "schema": PROTO_SCHEMA,
512 "schemaType": "PROTOBUF",
513 "messageType": message_type,
514 })))
515 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
516 "id": id,
517 })))
518 .expect(1)
519 .mount(&server)
520 .await;
521 }
522
523 let encoder = PostgresProtoEncoder::from_registry(&server.uri())
524 .await
525 .expect("registry allocates both IDs");
526 let difference = sample_difference();
527 let key = encoder.encode_key(&difference.key).expect("key encodes");
528 let value = encoder.encode_value(&difference).expect("value encodes");
529
530 assert2::assert!(decode_protobuf(&key).expect("key frame").0 == 73);
531 assert2::assert!(decode_protobuf(&value).expect("value frame").0 == 91);
532 }
533
534 #[test]
535 fn decoded_int8_key_encodes_as_int_scalar_kind() {
536 let mut cache = RelationCache::default();
537 cache.apply_relation(RelationEvent {
538 relation_id: RelationId(7),
539 schema: "public".to_owned(),
540 table: "orders".to_owned(),
541 columns: vec![ColumnSchema {
542 name: "id".to_owned(),
543 type_name: "int8".to_owned(),
544 key: true,
545 }],
546 });
547 let difference = cache
548 .translate(RowEvent {
549 relation_id: RelationId(7),
550 lsn: PgLsn(0x2a),
551 commit_lsn: None,
552 txid: None,
553 commit_timestamp_ms: None,
554 kind: RowEventKind::Insert,
555 values: vec![ColumnValue {
556 name: "col0".to_owned(),
557 value: ScalarValue::Text("42".to_owned()),
558 }],
559 })
560 .expect("decoded row should translate");
561 assert2::assert!(
562 &difference
563 == &EntityDifference {
564 table: "public.orders".into(),
565 key: EntityKey {
566 table: "public.orders".into(),
567 columns: vec![ColumnValue {
568 name: "id".into(),
569 value: ScalarValue::Int(42),
570 }],
571 },
572 op: Operation::Insert,
573 before: vec![],
574 after: vec![ColumnValue {
575 name: "id".into(),
576 value: ScalarValue::Int(42),
577 }],
578 lsn: PgLsn(0x2a),
579 txid: None,
580 commit_timestamp_ms: None,
581 schema: TableSchema {
582 schema: "public".into(),
583 table: "orders".into(),
584 columns: vec![ColumnSchema {
585 name: "id".into(),
586 type_name: "int8".into(),
587 key: true,
588 }],
589 },
590 }
591 );
592
593 let encoder =
594 PostgresProtoEncoder::with_schema_ids(41, 57).expect("encoder builds descriptors");
595 let key = encoder.encode_key(&difference.key).expect("key encodes");
596 let pool = DescriptorPool::from_file_descriptor_set(schema_descriptor_set())
597 .expect("descriptor pool builds");
598 let (_, _, key_body) = decode_protobuf(&key).expect("key frame decodes");
599 let key_message = DynamicMessage::decode(
600 message_descriptor(&pool, ENTITY_KEY).expect("key descriptor"),
601 key_body,
602 )
603 .expect("key body decodes");
604 let key_columns = list_field(&key_message, "columns");
605 let id_column = message_value(&key_columns[0]);
606
607 assert2::assert!(string_field(id_column, "kind") == "int".to_string());
608 assert2::assert!(i64_field(id_column, "int_value") == 42);
609 }
610
611 #[test]
612 fn descriptor_fields_have_expected_proto3_labels() {
613 let descriptor_set = schema_descriptor_set();
614 let file = descriptor_set.file.first().expect("descriptor file");
615 let column_value = file
616 .message_type
617 .iter()
618 .find(|message| message.name.as_deref() == Some(COLUMN_VALUE))
619 .expect("column value message");
620 let entity_key = file
621 .message_type
622 .iter()
623 .find(|message| message.name.as_deref() == Some(ENTITY_KEY))
624 .expect("entity key message");
625 let entity_difference = file
626 .message_type
627 .iter()
628 .find(|message| message.name.as_deref() == Some(ENTITY_DIFFERENCE))
629 .expect("entity difference message");
630
631 assert2::assert!(
632 [
633 column_value
634 .field
635 .iter()
636 .find(|field| field.name.as_deref() == Some("name"))
637 .and_then(|field| field.label),
638 entity_key
639 .field
640 .iter()
641 .find(|field| field.name.as_deref() == Some("columns"))
642 .and_then(|field| field.label),
643 entity_difference
644 .field
645 .iter()
646 .find(|field| field.name.as_deref() == Some("key"))
647 .and_then(|field| field.label),
648 ] == [
649 Some(prost_reflect::prost_types::field_descriptor_proto::Label::Optional as i32),
650 Some(prost_reflect::prost_types::field_descriptor_proto::Label::Repeated as i32),
651 Some(prost_reflect::prost_types::field_descriptor_proto::Label::Optional as i32),
652 ]
653 );
654 }
655
656 fn sample_difference() -> EntityDifference {
657 let key = EntityKey {
658 table: "public.accounts".to_owned(),
659 columns: vec![ColumnValue {
660 name: "id".to_owned(),
661 value: ScalarValue::Int(42),
662 }],
663 };
664
665 EntityDifference {
666 table: "public.accounts".to_owned(),
667 key,
668 op: Operation::Update,
669 before: vec![
670 ColumnValue {
671 name: "name".to_owned(),
672 value: ScalarValue::Text("old".to_owned()),
673 },
674 ColumnValue {
675 name: "nickname".to_owned(),
676 value: ScalarValue::Null,
677 },
678 ],
679 after: vec![
680 ColumnValue {
681 name: "name".to_owned(),
682 value: ScalarValue::Text("new".to_owned()),
683 },
684 ColumnValue {
685 name: "avatar".to_owned(),
686 value: ScalarValue::Bytes(b"abc".to_vec()),
687 },
688 ColumnValue {
689 name: "details".to_owned(),
690 value: ScalarValue::UnchangedToast,
691 },
692 ],
693 lsn: PgLsn(42),
694 txid: Some(TransactionId(7)),
695 commit_timestamp_ms: Some(1_700_000_000_000),
696 schema: TableSchema {
697 schema: "public".to_owned(),
698 table: "accounts".to_owned(),
699 columns: vec![
700 ColumnSchema {
701 name: "id".to_owned(),
702 type_name: "int8".to_owned(),
703 key: true,
704 },
705 ColumnSchema {
706 name: "name".to_owned(),
707 type_name: "text".to_owned(),
708 key: false,
709 },
710 ],
711 },
712 }
713 }
714
715 fn string_field(message: &DynamicMessage, name: &str) -> String {
716 match message
717 .get_field_by_name(name)
718 .expect("field exists")
719 .as_ref()
720 {
721 Value::String(value) => value.clone(),
722 other => panic!("field {name} was not a string: {other:?}"),
723 }
724 }
725
726 fn bool_field(message: &DynamicMessage, name: &str) -> bool {
727 match message
728 .get_field_by_name(name)
729 .expect("field exists")
730 .as_ref()
731 {
732 Value::Bool(value) => *value,
733 other => panic!("field {name} was not a bool: {other:?}"),
734 }
735 }
736
737 fn i64_field(message: &DynamicMessage, name: &str) -> i64 {
738 match message
739 .get_field_by_name(name)
740 .expect("field exists")
741 .as_ref()
742 {
743 Value::I64(value) => *value,
744 other => panic!("field {name} was not an int64: {other:?}"),
745 }
746 }
747
748 fn bytes_field(message: &DynamicMessage, name: &str) -> Bytes {
749 match message
750 .get_field_by_name(name)
751 .expect("field exists")
752 .as_ref()
753 {
754 Value::Bytes(value) => value.clone(),
755 other => panic!("field {name} was not bytes: {other:?}"),
756 }
757 }
758
759 fn list_field(message: &DynamicMessage, name: &str) -> Vec<Value> {
760 match message
761 .get_field_by_name(name)
762 .expect("field exists")
763 .as_ref()
764 {
765 Value::List(value) => value.clone(),
766 other => panic!("field {name} was not a list: {other:?}"),
767 }
768 }
769
770 fn message_value(value: &Value) -> &DynamicMessage {
771 match value {
772 Value::Message(message) => message,
773 other => panic!("value was not a message: {other:?}"),
774 }
775 }
776}