Skip to main content

grid_sdk/protocol/product/
payload.rs

1// Copyright (c) 2019 Target Brands, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Protocol structs for Product transaction payloads
16
17use protobuf::Message;
18use protobuf::RepeatedField;
19
20use std::error::Error as StdError;
21
22use super::errors::BuilderError;
23
24use crate::protocol::{product::state::ProductNamespace, schema::state::PropertyValue};
25use crate::protos;
26use crate::protos::{product_payload, product_payload::ProductPayload_Action};
27use crate::protos::{
28    FromBytes, FromNative, FromProto, IntoBytes, IntoNative, IntoProto, ProtoConversionError,
29};
30
31/// The Product payload's action envelope
32#[derive(Debug, Clone, PartialEq)]
33pub enum Action {
34    ProductCreate(ProductCreateAction),
35    ProductUpdate(ProductUpdateAction),
36    ProductDelete(ProductDeleteAction),
37}
38
39/// Native representation of a Product transaction payload
40#[derive(Debug, Clone, PartialEq)]
41pub struct ProductPayload {
42    action: Action,
43    timestamp: u64,
44}
45
46impl ProductPayload {
47    pub fn action(&self) -> &Action {
48        &self.action
49    }
50    pub fn timestamp(&self) -> &u64 {
51        &self.timestamp
52    }
53}
54
55impl FromProto<protos::product_payload::ProductPayload> for ProductPayload {
56    fn from_proto(
57        payload: protos::product_payload::ProductPayload,
58    ) -> Result<Self, ProtoConversionError> {
59        let action = match payload.get_action() {
60            ProductPayload_Action::PRODUCT_CREATE => Action::ProductCreate(
61                ProductCreateAction::from_proto(payload.get_product_create().clone())?,
62            ),
63            ProductPayload_Action::PRODUCT_UPDATE => Action::ProductUpdate(
64                ProductUpdateAction::from_proto(payload.get_product_update().clone())?,
65            ),
66            ProductPayload_Action::PRODUCT_DELETE => Action::ProductDelete(
67                ProductDeleteAction::from_proto(payload.get_product_delete().clone())?,
68            ),
69            ProductPayload_Action::UNSET_ACTION => {
70                return Err(ProtoConversionError::InvalidTypeError(
71                    "Cannot convert ProductPayload_Action with type unset".to_string(),
72                ));
73            }
74        };
75        Ok(ProductPayload {
76            action,
77            timestamp: payload.get_timestamp(),
78        })
79    }
80}
81
82impl FromNative<ProductPayload> for protos::product_payload::ProductPayload {
83    fn from_native(native: ProductPayload) -> Result<Self, ProtoConversionError> {
84        let mut proto = product_payload::ProductPayload::new();
85
86        proto.set_timestamp(*native.timestamp());
87
88        match native.action() {
89            Action::ProductCreate(payload) => {
90                proto.set_action(ProductPayload_Action::PRODUCT_CREATE);
91                proto.set_product_create(payload.clone().into_proto()?);
92            }
93            Action::ProductUpdate(payload) => {
94                proto.set_action(ProductPayload_Action::PRODUCT_UPDATE);
95                proto.set_product_update(payload.clone().into_proto()?);
96            }
97            Action::ProductDelete(payload) => {
98                proto.set_action(ProductPayload_Action::PRODUCT_DELETE);
99                proto.set_product_delete(payload.clone().into_proto()?);
100            }
101        }
102
103        Ok(proto)
104    }
105}
106
107impl FromBytes<ProductPayload> for ProductPayload {
108    fn from_bytes(bytes: &[u8]) -> Result<ProductPayload, ProtoConversionError> {
109        let proto: product_payload::ProductPayload =
110            Message::parse_from_bytes(bytes).map_err(|_| {
111                ProtoConversionError::SerializationError(
112                    "Unable to get ProductPayload from bytes".into(),
113                )
114            })?;
115        proto.into_native()
116    }
117}
118
119impl IntoBytes for ProductPayload {
120    fn into_bytes(self) -> Result<Vec<u8>, ProtoConversionError> {
121        let proto = self.into_proto()?;
122        let bytes = proto.write_to_bytes().map_err(|_| {
123            ProtoConversionError::SerializationError(
124                "Unable to get ProductPayload from bytes".into(),
125            )
126        })?;
127        Ok(bytes)
128    }
129}
130
131impl IntoProto<protos::product_payload::ProductPayload> for ProductPayload {}
132impl IntoNative<ProductPayload> for protos::product_payload::ProductPayload {}
133
134/// Returned if any required fields in a `ProductPayload` are not present when being
135/// converted from the corresponding builder
136#[derive(Debug)]
137pub enum ProductPayloadBuildError {
138    MissingField(String),
139}
140
141impl StdError for ProductPayloadBuildError {
142    fn description(&self) -> &str {
143        match *self {
144            ProductPayloadBuildError::MissingField(ref msg) => msg,
145        }
146    }
147
148    fn cause(&self) -> Option<&dyn StdError> {
149        match *self {
150            ProductPayloadBuildError::MissingField(_) => None,
151        }
152    }
153}
154
155impl std::fmt::Display for ProductPayloadBuildError {
156    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
157        match *self {
158            ProductPayloadBuildError::MissingField(ref s) => write!(f, "missing field \"{}\"", s),
159        }
160    }
161}
162
163/// Builder used to create a `ProductPayload`
164#[derive(Default, Clone)]
165pub struct ProductPayloadBuilder {
166    action: Option<Action>,
167    timestamp: Option<u64>,
168}
169
170impl ProductPayloadBuilder {
171    pub fn new() -> Self {
172        ProductPayloadBuilder::default()
173    }
174    pub fn with_action(mut self, action: Action) -> Self {
175        self.action = Some(action);
176        self
177    }
178    pub fn with_timestamp(mut self, value: u64) -> Self {
179        self.timestamp = Some(value);
180        self
181    }
182    pub fn build(self) -> Result<ProductPayload, BuilderError> {
183        let action = self
184            .action
185            .ok_or_else(|| BuilderError::MissingField("'action' field is required".into()))?;
186        let timestamp = self
187            .timestamp
188            .ok_or_else(|| BuilderError::MissingField("'timestamp' field is required".into()))?;
189        Ok(ProductPayload { action, timestamp })
190    }
191}
192
193/// Native representation of the "create product" action payload
194#[derive(Debug, Default, Clone, PartialEq)]
195pub struct ProductCreateAction {
196    product_namespace: ProductNamespace,
197    product_id: String,
198    owner: String,
199    properties: Vec<PropertyValue>,
200}
201
202impl ProductCreateAction {
203    pub fn product_namespace(&self) -> &ProductNamespace {
204        &self.product_namespace
205    }
206
207    pub fn product_id(&self) -> &str {
208        &self.product_id
209    }
210
211    pub fn owner(&self) -> &str {
212        &self.owner
213    }
214
215    pub fn properties(&self) -> &[PropertyValue] {
216        &self.properties
217    }
218}
219
220impl FromProto<product_payload::ProductCreateAction> for ProductCreateAction {
221    fn from_proto(
222        proto: product_payload::ProductCreateAction,
223    ) -> Result<Self, ProtoConversionError> {
224        Ok(ProductCreateAction {
225            product_namespace: ProductNamespace::from_proto(proto.get_product_namespace())?,
226            product_id: proto.get_product_id().to_string(),
227            owner: proto.get_owner().to_string(),
228            properties: proto
229                .get_properties()
230                .iter()
231                .cloned()
232                .map(PropertyValue::from_proto)
233                .collect::<Result<Vec<PropertyValue>, ProtoConversionError>>()?,
234        })
235    }
236}
237
238impl FromNative<ProductCreateAction> for product_payload::ProductCreateAction {
239    fn from_native(native: ProductCreateAction) -> Result<Self, ProtoConversionError> {
240        let mut proto = protos::product_payload::ProductCreateAction::new();
241        proto.set_product_namespace(native.product_namespace().clone().into_proto()?);
242        proto.set_product_id(native.product_id().to_string());
243        proto.set_owner(native.owner().to_string());
244        proto.set_properties(RepeatedField::from_vec(
245            native
246                .properties()
247                .iter()
248                .cloned()
249                .map(PropertyValue::into_proto)
250                .collect::<Result<Vec<protos::schema_state::PropertyValue>, ProtoConversionError>>(
251                )?,
252        ));
253        Ok(proto)
254    }
255}
256
257impl FromBytes<ProductCreateAction> for ProductCreateAction {
258    fn from_bytes(bytes: &[u8]) -> Result<ProductCreateAction, ProtoConversionError> {
259        let proto: protos::product_payload::ProductCreateAction = Message::parse_from_bytes(bytes)
260            .map_err(|_| {
261                ProtoConversionError::SerializationError(
262                    "Unable to get ProductCreateAction from bytes".to_string(),
263                )
264            })?;
265        proto.into_native()
266    }
267}
268
269impl IntoBytes for ProductCreateAction {
270    fn into_bytes(self) -> Result<Vec<u8>, ProtoConversionError> {
271        let proto = self.into_proto()?;
272        let bytes = proto.write_to_bytes().map_err(|_| {
273            ProtoConversionError::SerializationError(
274                "Unable to get bytes from ProductCreateAction".to_string(),
275            )
276        })?;
277        Ok(bytes)
278    }
279}
280
281impl IntoProto<protos::product_payload::ProductCreateAction> for ProductCreateAction {}
282impl IntoNative<ProductCreateAction> for protos::product_payload::ProductCreateAction {}
283
284/// Builder used to create a "create product" action payload
285#[derive(Default, Debug)]
286pub struct ProductCreateActionBuilder {
287    product_namespace: Option<ProductNamespace>,
288    product_id: Option<String>,
289    owner: Option<String>,
290    properties: Option<Vec<PropertyValue>>,
291}
292
293impl ProductCreateActionBuilder {
294    pub fn new() -> Self {
295        ProductCreateActionBuilder::default()
296    }
297    pub fn with_product_namespace(mut self, value: ProductNamespace) -> Self {
298        self.product_namespace = Some(value);
299        self
300    }
301    pub fn with_product_id(mut self, value: String) -> Self {
302        self.product_id = Some(value);
303        self
304    }
305    pub fn with_owner(mut self, value: String) -> Self {
306        self.owner = Some(value);
307        self
308    }
309    pub fn with_properties(mut self, value: Vec<PropertyValue>) -> Self {
310        self.properties = Some(value);
311        self
312    }
313    pub fn build(self) -> Result<ProductCreateAction, BuilderError> {
314        let product_namespace = self.product_namespace.ok_or_else(|| {
315            BuilderError::MissingField("'product_namespace' field is required".to_string())
316        })?;
317        let product_id = self
318            .product_id
319            .ok_or_else(|| BuilderError::MissingField("'product_id' field is required".into()))?;
320        let owner = self
321            .owner
322            .ok_or_else(|| BuilderError::MissingField("'owner' field is required".into()))?;
323        let properties = self
324            .properties
325            .ok_or_else(|| BuilderError::MissingField("'properties' field is required".into()))?;
326        Ok(ProductCreateAction {
327            product_namespace,
328            product_id,
329            owner,
330            properties,
331        })
332    }
333}
334
335/// Native representation of an "update product" action payload
336#[derive(Debug, Default, Clone, PartialEq)]
337pub struct ProductUpdateAction {
338    product_namespace: ProductNamespace,
339    product_id: String,
340    properties: Vec<PropertyValue>,
341}
342
343impl ProductUpdateAction {
344    pub fn product_namespace(&self) -> &ProductNamespace {
345        &self.product_namespace
346    }
347
348    pub fn product_id(&self) -> &str {
349        &self.product_id
350    }
351
352    pub fn properties(&self) -> &[PropertyValue] {
353        &self.properties
354    }
355}
356
357impl FromProto<protos::product_payload::ProductUpdateAction> for ProductUpdateAction {
358    fn from_proto(
359        proto: protos::product_payload::ProductUpdateAction,
360    ) -> Result<Self, ProtoConversionError> {
361        Ok(ProductUpdateAction {
362            product_namespace: ProductNamespace::from_proto(proto.get_product_namespace())?,
363            product_id: proto.get_product_id().to_string(),
364            properties: proto
365                .get_properties()
366                .iter()
367                .cloned()
368                .map(PropertyValue::from_proto)
369                .collect::<Result<Vec<PropertyValue>, ProtoConversionError>>()?,
370        })
371    }
372}
373
374impl FromNative<ProductUpdateAction> for protos::product_payload::ProductUpdateAction {
375    fn from_native(native: ProductUpdateAction) -> Result<Self, ProtoConversionError> {
376        let mut proto = protos::product_payload::ProductUpdateAction::new();
377        proto.set_product_namespace(native.product_namespace().clone().into_proto()?);
378        proto.set_product_id(native.product_id().to_string());
379        proto.set_properties(RepeatedField::from_vec(
380            native
381                .properties()
382                .iter()
383                .cloned()
384                .map(PropertyValue::into_proto)
385                .collect::<Result<Vec<protos::schema_state::PropertyValue>, ProtoConversionError>>(
386                )?,
387        ));
388
389        Ok(proto)
390    }
391}
392
393impl FromBytes<ProductUpdateAction> for ProductUpdateAction {
394    fn from_bytes(bytes: &[u8]) -> Result<ProductUpdateAction, ProtoConversionError> {
395        let proto: protos::product_payload::ProductUpdateAction = Message::parse_from_bytes(bytes)
396            .map_err(|_| {
397                ProtoConversionError::SerializationError(
398                    "Unable to get ProductUpdateAction from bytes".to_string(),
399                )
400            })?;
401        proto.into_native()
402    }
403}
404
405impl IntoBytes for ProductUpdateAction {
406    fn into_bytes(self) -> Result<Vec<u8>, ProtoConversionError> {
407        let proto = self.into_proto()?;
408        let bytes = proto.write_to_bytes().map_err(|_| {
409            ProtoConversionError::SerializationError(
410                "Unable to get bytes from ProductUpdateAction".to_string(),
411            )
412        })?;
413        Ok(bytes)
414    }
415}
416
417impl IntoProto<protos::product_payload::ProductUpdateAction> for ProductUpdateAction {}
418impl IntoNative<ProductUpdateAction> for protos::product_payload::ProductUpdateAction {}
419
420/// Builder used to create an "update product" action
421#[derive(Default, Clone)]
422pub struct ProductUpdateActionBuilder {
423    product_namespace: Option<ProductNamespace>,
424    product_id: Option<String>,
425    properties: Vec<PropertyValue>,
426}
427
428impl ProductUpdateActionBuilder {
429    pub fn new() -> Self {
430        ProductUpdateActionBuilder::default()
431    }
432
433    pub fn with_product_namespace(mut self, product_namespace: ProductNamespace) -> Self {
434        self.product_namespace = Some(product_namespace);
435        self
436    }
437
438    pub fn with_product_id(mut self, product_id: String) -> Self {
439        self.product_id = Some(product_id);
440        self
441    }
442
443    pub fn with_properties(mut self, properties: Vec<PropertyValue>) -> Self {
444        self.properties = properties;
445        self
446    }
447
448    pub fn build(self) -> Result<ProductUpdateAction, BuilderError> {
449        let product_namespace = self.product_namespace.ok_or_else(|| {
450            BuilderError::MissingField("'product_namespace' field is required".to_string())
451        })?;
452
453        let product_id = self.product_id.ok_or_else(|| {
454            BuilderError::MissingField("'product_id' field is required".to_string())
455        })?;
456
457        let properties = {
458            if !self.properties.is_empty() {
459                self.properties
460            } else {
461                return Err(BuilderError::MissingField(
462                    "'properties' field is required".to_string(),
463                ));
464            }
465        };
466
467        Ok(ProductUpdateAction {
468            product_namespace,
469            product_id,
470            properties,
471        })
472    }
473}
474
475/// Native representation of the "delete product" action payload
476#[derive(Debug, Default, Clone, PartialEq)]
477pub struct ProductDeleteAction {
478    product_namespace: ProductNamespace,
479    product_id: String,
480}
481
482impl ProductDeleteAction {
483    pub fn product_namespace(&self) -> &ProductNamespace {
484        &self.product_namespace
485    }
486
487    pub fn product_id(&self) -> &str {
488        &self.product_id
489    }
490}
491
492impl FromProto<protos::product_payload::ProductDeleteAction> for ProductDeleteAction {
493    fn from_proto(
494        proto: protos::product_payload::ProductDeleteAction,
495    ) -> Result<Self, ProtoConversionError> {
496        Ok(ProductDeleteAction {
497            product_namespace: ProductNamespace::from_proto(proto.get_product_namespace())?,
498            product_id: proto.get_product_id().to_string(),
499        })
500    }
501}
502
503impl FromNative<ProductDeleteAction> for protos::product_payload::ProductDeleteAction {
504    fn from_native(native: ProductDeleteAction) -> Result<Self, ProtoConversionError> {
505        let mut proto = protos::product_payload::ProductDeleteAction::new();
506        proto.set_product_namespace(native.product_namespace().clone().into_proto()?);
507        proto.set_product_id(native.product_id().to_string());
508        Ok(proto)
509    }
510}
511
512impl FromBytes<ProductDeleteAction> for ProductDeleteAction {
513    fn from_bytes(bytes: &[u8]) -> Result<ProductDeleteAction, ProtoConversionError> {
514        let proto: protos::product_payload::ProductDeleteAction = Message::parse_from_bytes(bytes)
515            .map_err(|_| {
516                ProtoConversionError::SerializationError(
517                    "Unable to get ProductDeleteAction from bytes".to_string(),
518                )
519            })?;
520        proto.into_native()
521    }
522}
523
524impl IntoBytes for ProductDeleteAction {
525    fn into_bytes(self) -> Result<Vec<u8>, ProtoConversionError> {
526        let proto = self.into_proto()?;
527        let bytes = proto.write_to_bytes().map_err(|_| {
528            ProtoConversionError::SerializationError(
529                "Unable to get bytes from ProductDeleteAction".to_string(),
530            )
531        })?;
532        Ok(bytes)
533    }
534}
535
536impl IntoProto<protos::product_payload::ProductDeleteAction> for ProductDeleteAction {}
537impl IntoNative<ProductDeleteAction> for protos::product_payload::ProductDeleteAction {}
538
539/// Builder used to create a "delete product" action
540#[derive(Default, Clone)]
541pub struct ProductDeleteActionBuilder {
542    product_namespace: Option<ProductNamespace>,
543    product_id: Option<String>,
544}
545
546impl ProductDeleteActionBuilder {
547    pub fn new() -> Self {
548        ProductDeleteActionBuilder::default()
549    }
550
551    pub fn with_product_namespace(mut self, product_namespace: ProductNamespace) -> Self {
552        self.product_namespace = Some(product_namespace);
553        self
554    }
555
556    pub fn with_product_id(mut self, product_id: String) -> Self {
557        self.product_id = Some(product_id);
558        self
559    }
560
561    pub fn build(self) -> Result<ProductDeleteAction, BuilderError> {
562        let product_namespace = self.product_namespace.ok_or_else(|| {
563            BuilderError::MissingField("'product_namespace' field is required".to_string())
564        })?;
565
566        let product_id = self.product_id.ok_or_else(|| {
567            BuilderError::MissingField("'product_id' field is required".to_string())
568        })?;
569
570        Ok(ProductDeleteAction {
571            product_namespace,
572            product_id,
573        })
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use crate::protocol::schema::state::{DataType, PropertyValueBuilder};
581    use std::fmt::Debug;
582
583    #[test]
584    /// Validate that a `ProductCreateAction` is built correctly
585    fn test_product_create_builder() {
586        let action = ProductCreateActionBuilder::new()
587            .with_product_id("688955434684".into()) // GTIN-12
588            .with_product_namespace(ProductNamespace::Gs1)
589            .with_owner("Target".into())
590            .with_properties(make_properties())
591            .build()
592            .unwrap();
593
594        assert_eq!(action.product_id(), "688955434684");
595        assert_eq!(action.owner(), "Target");
596        assert_eq!(*action.product_namespace(), ProductNamespace::Gs1);
597        assert_eq!(action.properties()[0].name(), "description");
598        assert_eq!(*action.properties()[0].data_type(), DataType::String);
599        assert_eq!(
600            action.properties()[0].string_value(),
601            "This is a product description"
602        );
603        assert_eq!(action.properties()[1].name(), "price");
604        assert_eq!(*action.properties()[1].data_type(), DataType::Number);
605        assert_eq!(*action.properties()[1].number_value(), 3);
606    }
607
608    #[test]
609    /// Validate that a `ProductCreateAction` may be correctly converted into bytes and back
610    /// to its native representation
611    fn test_product_create_into_bytes() {
612        let action = ProductCreateActionBuilder::new()
613            .with_product_id("688955434684".into()) // GTIN-12
614            .with_product_namespace(ProductNamespace::Gs1)
615            .with_owner("Target".into())
616            .with_properties(make_properties())
617            .build()
618            .unwrap();
619
620        test_from_bytes(action, ProductCreateAction::from_bytes);
621    }
622
623    #[test]
624    /// Validate that a `ProductUpdateAction` is built correctly
625    fn test_product_update_builder() {
626        let action = ProductUpdateActionBuilder::new()
627            .with_product_id("688955434684".into()) // GTIN-12
628            .with_product_namespace(ProductNamespace::Gs1)
629            .with_properties(make_properties())
630            .build()
631            .unwrap();
632
633        assert_eq!(action.product_id(), "688955434684");
634        assert_eq!(*action.product_namespace(), ProductNamespace::Gs1);
635        assert_eq!(action.properties()[0].name(), "description");
636        assert_eq!(*action.properties()[0].data_type(), DataType::String);
637        assert_eq!(
638            action.properties()[0].string_value(),
639            "This is a product description"
640        );
641        assert_eq!(action.properties()[1].name(), "price");
642        assert_eq!(*action.properties()[1].data_type(), DataType::Number);
643        assert_eq!(*action.properties()[1].number_value(), 3);
644    }
645
646    #[test]
647    /// Validate that an `ProductUpdateAction` may be correctly converted into bytes and back
648    /// to its native representation
649    fn test_product_update_into_bytes() {
650        let action = ProductUpdateActionBuilder::new()
651            .with_product_id("688955434684".into()) // GTIN-12
652            .with_product_namespace(ProductNamespace::Gs1)
653            .with_properties(make_properties())
654            .build()
655            .unwrap();
656
657        test_from_bytes(action, ProductUpdateAction::from_bytes);
658    }
659
660    #[test]
661    /// Validate that an `ProductDeleteAction` may be built correctly
662    fn test_product_delete_builder() {
663        let action = ProductDeleteActionBuilder::new()
664            .with_product_id("688955434684".into()) // GTIN-12
665            .with_product_namespace(ProductNamespace::Gs1)
666            .build()
667            .unwrap();
668
669        assert_eq!(action.product_id(), "688955434684");
670        assert_eq!(*action.product_namespace(), ProductNamespace::Gs1);
671    }
672
673    #[test]
674    /// Validate that a `ProductDeleteAction` may be correctly converted into bytes and back
675    /// to its native representation
676    fn test_product_delete_into_bytes() {
677        let action = ProductDeleteActionBuilder::new()
678            .with_product_id("688955434684".into()) // GTIN-12
679            .with_product_namespace(ProductNamespace::Gs1)
680            .build()
681            .unwrap();
682
683        test_from_bytes(action, ProductDeleteAction::from_bytes);
684    }
685
686    #[test]
687    /// Validate that a `ProductPayload` is built correctly with a `ProductCreateAction`
688    fn test_product_payload_builder() {
689        let action = ProductCreateActionBuilder::new()
690            .with_product_id("688955434684".into()) // GTIN-12
691            .with_product_namespace(ProductNamespace::Gs1)
692            .with_owner("Target".into())
693            .with_properties(make_properties())
694            .build()
695            .unwrap();
696
697        let payload = ProductPayloadBuilder::new()
698            .with_action(Action::ProductCreate(action.clone()))
699            .with_timestamp(0)
700            .build()
701            .unwrap();
702
703        assert_eq!(*payload.action(), Action::ProductCreate(action));
704        assert_eq!(*payload.timestamp(), 0);
705    }
706
707    #[test]
708    /// Validate that a `ProductPayload` with a `ProductCreateAction` may be correctly converted
709    /// into bytes and back to its native representation
710    fn test_product_payload_bytes() {
711        let action = ProductCreateActionBuilder::new()
712            .with_product_id("688955434684".into()) // GTIN-12
713            .with_product_namespace(ProductNamespace::Gs1)
714            .with_owner("Target".into())
715            .with_properties(make_properties())
716            .build()
717            .unwrap();
718
719        let payload = ProductPayloadBuilder::new()
720            .with_action(Action::ProductCreate(action.clone()))
721            .with_timestamp(0)
722            .build()
723            .unwrap();
724
725        test_from_bytes(payload, ProductPayload::from_bytes);
726    }
727
728    fn make_properties() -> Vec<PropertyValue> {
729        let property_value_description = PropertyValueBuilder::new()
730            .with_name("description".into())
731            .with_data_type(DataType::String)
732            .with_string_value("This is a product description".into())
733            .build()
734            .unwrap();
735        let property_value_price = PropertyValueBuilder::new()
736            .with_name("price".into())
737            .with_data_type(DataType::Number)
738            .with_number_value(3)
739            .build()
740            .unwrap();
741
742        vec![
743            property_value_description.clone(),
744            property_value_price.clone(),
745        ]
746    }
747
748    fn test_from_bytes<T: FromBytes<T> + Clone + PartialEq + IntoBytes + Debug, F>(
749        under_test: T,
750        from_bytes: F,
751    ) where
752        F: Fn(&[u8]) -> Result<T, ProtoConversionError>,
753    {
754        let bytes = under_test.clone().into_bytes().unwrap();
755        let created_from_bytes = from_bytes(&bytes).unwrap();
756        assert_eq!(under_test, created_from_bytes);
757    }
758}