grid_sdk/protocol/product/
state.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 state
16
17use protobuf::Message;
18use protobuf::RepeatedField;
19
20use std::error::Error as StdError;
21
22use crate::protos;
23use crate::protos::schema_state;
24use crate::protos::{
25    FromBytes, FromNative, FromProto, IntoBytes, IntoNative, IntoProto, ProtoConversionError,
26};
27
28use crate::protocol::schema::state::PropertyValue;
29
30/// Possible Product namespaces
31///
32/// The namespace determines the schema used to define a `Product`'s properties
33#[derive(Debug, Clone, PartialEq)]
34pub enum ProductNamespace {
35    Gs1,
36}
37
38impl Default for ProductNamespace {
39    fn default() -> Self {
40        ProductNamespace::Gs1
41    }
42}
43
44impl FromProto<protos::product_state::Product_ProductNamespace> for ProductNamespace {
45    fn from_proto(
46        product_namespace: protos::product_state::Product_ProductNamespace,
47    ) -> Result<Self, ProtoConversionError> {
48        match product_namespace {
49            protos::product_state::Product_ProductNamespace::GS1 => Ok(ProductNamespace::Gs1),
50            protos::product_state::Product_ProductNamespace::UNSET_TYPE => {
51                Err(ProtoConversionError::InvalidTypeError(
52                    "Cannot convert Product_ProductNamespace with type UNSET_TYPE".to_string(),
53                ))
54            }
55        }
56    }
57}
58
59impl FromNative<ProductNamespace> for protos::product_state::Product_ProductNamespace {
60    fn from_native(product_namespace: ProductNamespace) -> Result<Self, ProtoConversionError> {
61        match product_namespace {
62            ProductNamespace::Gs1 => Ok(protos::product_state::Product_ProductNamespace::GS1),
63        }
64    }
65}
66
67impl IntoProto<protos::product_state::Product_ProductNamespace> for ProductNamespace {}
68impl IntoNative<ProductNamespace> for protos::product_state::Product_ProductNamespace {}
69
70/// Native representation of `Product`
71///
72/// A `Product` contains a list of properties determined by the `product_namespace`.
73#[derive(Debug, Clone, PartialEq)]
74pub struct Product {
75    product_id: String,
76    product_namespace: ProductNamespace,
77    owner: String,
78    properties: Vec<PropertyValue>,
79}
80
81impl Product {
82    pub fn product_id(&self) -> &str {
83        &self.product_id
84    }
85
86    pub fn product_namespace(&self) -> &ProductNamespace {
87        &self.product_namespace
88    }
89
90    pub fn owner(&self) -> &str {
91        &self.owner
92    }
93
94    pub fn properties(&self) -> &[PropertyValue] {
95        &self.properties
96    }
97
98    pub fn into_builder(self) -> ProductBuilder {
99        ProductBuilder::new()
100            .with_product_id(self.product_id)
101            .with_product_namespace(self.product_namespace)
102            .with_owner(self.owner)
103            .with_properties(self.properties)
104    }
105}
106
107impl FromProto<protos::product_state::Product> for Product {
108    fn from_proto(product: protos::product_state::Product) -> Result<Self, ProtoConversionError> {
109        Ok(Product {
110            product_id: product.get_product_id().to_string(),
111            product_namespace: ProductNamespace::from_proto(product.get_product_namespace())?,
112            owner: product.get_owner().to_string(),
113            properties: product
114                .get_properties()
115                .iter()
116                .cloned()
117                .map(PropertyValue::from_proto)
118                .collect::<Result<Vec<PropertyValue>, ProtoConversionError>>()?,
119        })
120    }
121}
122
123impl FromNative<Product> for protos::product_state::Product {
124    fn from_native(product: Product) -> Result<Self, ProtoConversionError> {
125        let mut proto = protos::product_state::Product::new();
126        proto.set_product_id(product.product_id().to_string());
127        proto.set_product_namespace(product.product_namespace().clone().into_proto()?);
128        proto.set_owner(product.owner().to_string());
129        proto.set_properties(RepeatedField::from_vec(
130            product
131                .properties()
132                .iter()
133                .cloned()
134                .map(PropertyValue::into_proto)
135                .collect::<Result<Vec<schema_state::PropertyValue>, ProtoConversionError>>()?,
136        ));
137        Ok(proto)
138    }
139}
140
141impl FromBytes<Product> for Product {
142    fn from_bytes(bytes: &[u8]) -> Result<Product, ProtoConversionError> {
143        let proto: protos::product_state::Product =
144            Message::parse_from_bytes(bytes).map_err(|_| {
145                ProtoConversionError::SerializationError(
146                    "Unable to get Product from bytes".to_string(),
147                )
148            })?;
149        proto.into_native()
150    }
151}
152
153impl IntoBytes for Product {
154    fn into_bytes(self) -> Result<Vec<u8>, ProtoConversionError> {
155        let proto = self.into_proto()?;
156        let bytes = proto.write_to_bytes().map_err(|_| {
157            ProtoConversionError::SerializationError("Unable to get bytes from Product".to_string())
158        })?;
159        Ok(bytes)
160    }
161}
162
163impl IntoProto<protos::product_state::Product> for Product {}
164impl IntoNative<Product> for protos::product_state::Product {}
165
166/// Returned if any required fields in a `Product` are not present when being
167/// converted from the corresponding builder
168#[derive(Debug)]
169pub enum ProductBuildError {
170    MissingField(String),
171    EmptyVec(String),
172}
173
174impl StdError for ProductBuildError {
175    fn description(&self) -> &str {
176        match *self {
177            ProductBuildError::MissingField(ref msg) => msg,
178            ProductBuildError::EmptyVec(ref msg) => msg,
179        }
180    }
181
182    fn source(&self) -> Option<&(dyn StdError + 'static)> {
183        match *self {
184            ProductBuildError::MissingField(_) => None,
185            ProductBuildError::EmptyVec(_) => None,
186        }
187    }
188}
189
190impl std::fmt::Display for ProductBuildError {
191    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
192        match *self {
193            ProductBuildError::MissingField(ref s) => write!(f, "missing field \"{}\"", s),
194            ProductBuildError::EmptyVec(ref s) => write!(f, "\"{}\" must not be empty", s),
195        }
196    }
197}
198
199/// Builder used to create a `Product`
200#[derive(Default, Clone, PartialEq)]
201pub struct ProductBuilder {
202    pub product_id: Option<String>,
203    pub product_namespace: Option<ProductNamespace>,
204    pub owner: Option<String>,
205    pub properties: Option<Vec<PropertyValue>>,
206}
207
208impl ProductBuilder {
209    pub fn new() -> Self {
210        ProductBuilder::default()
211    }
212
213    pub fn with_product_id(mut self, product_id: String) -> Self {
214        self.product_id = Some(product_id);
215        self
216    }
217
218    pub fn with_product_namespace(mut self, product_namespace: ProductNamespace) -> Self {
219        self.product_namespace = Some(product_namespace);
220        self
221    }
222
223    pub fn with_owner(mut self, owner: String) -> Self {
224        self.owner = Some(owner);
225        self
226    }
227
228    pub fn with_properties(mut self, properties: Vec<PropertyValue>) -> Self {
229        self.properties = Some(properties);
230        self
231    }
232
233    pub fn build(self) -> Result<Product, ProductBuildError> {
234        let product_id = self.product_id.ok_or_else(|| {
235            ProductBuildError::MissingField("'product_id' field is required".to_string())
236        })?;
237
238        let product_namespace = self.product_namespace.ok_or_else(|| {
239            ProductBuildError::MissingField("'product_namespace' field is required".to_string())
240        })?;
241
242        let owner = self.owner.ok_or_else(|| {
243            ProductBuildError::MissingField("'owner' field is required".to_string())
244        })?;
245
246        // Product values are not required
247        let properties = self.properties.ok_or_else(|| {
248            ProductBuildError::MissingField("'properties' field is required".to_string())
249        })?;
250
251        Ok(Product {
252            product_id,
253            product_namespace,
254            owner,
255            properties,
256        })
257    }
258}
259
260/// Native representation of a list of `Product`s
261#[derive(Debug, Clone, PartialEq)]
262pub struct ProductList {
263    products: Vec<Product>,
264}
265
266impl ProductList {
267    pub fn products(&self) -> &[Product] {
268        &self.products
269    }
270
271    pub fn into_builder(self) -> ProductListBuilder {
272        ProductListBuilder::new().with_products(self.products)
273    }
274}
275
276impl FromProto<protos::product_state::ProductList> for ProductList {
277    fn from_proto(
278        product_list: protos::product_state::ProductList,
279    ) -> Result<Self, ProtoConversionError> {
280        Ok(ProductList {
281            products: product_list
282                .get_entries()
283                .iter()
284                .cloned()
285                .map(Product::from_proto)
286                .collect::<Result<Vec<Product>, ProtoConversionError>>()?,
287        })
288    }
289}
290
291impl FromNative<ProductList> for protos::product_state::ProductList {
292    fn from_native(product_list: ProductList) -> Result<Self, ProtoConversionError> {
293        let mut product_list_proto = protos::product_state::ProductList::new();
294
295        product_list_proto.set_entries(RepeatedField::from_vec(
296            product_list
297                .products()
298                .iter()
299                .cloned()
300                .map(Product::into_proto)
301                .collect::<Result<Vec<protos::product_state::Product>, ProtoConversionError>>()?,
302        ));
303
304        Ok(product_list_proto)
305    }
306}
307
308impl FromBytes<ProductList> for ProductList {
309    fn from_bytes(bytes: &[u8]) -> Result<ProductList, ProtoConversionError> {
310        let proto: protos::product_state::ProductList =
311            Message::parse_from_bytes(bytes).map_err(|_| {
312                ProtoConversionError::SerializationError(
313                    "Unable to get ProductList from bytes".to_string(),
314                )
315            })?;
316        proto.into_native()
317    }
318}
319
320impl IntoBytes for ProductList {
321    fn into_bytes(self) -> Result<Vec<u8>, ProtoConversionError> {
322        let proto = self.into_proto()?;
323        let bytes = proto.write_to_bytes().map_err(|_| {
324            ProtoConversionError::SerializationError(
325                "Unable to get bytes from ProductList".to_string(),
326            )
327        })?;
328        Ok(bytes)
329    }
330}
331
332impl IntoProto<protos::product_state::ProductList> for ProductList {}
333impl IntoNative<ProductList> for protos::product_state::ProductList {}
334
335/// Returned if any required fields in a `ProductList` are not present when being
336/// converted from the corresponding builder
337#[derive(Debug)]
338pub enum ProductListBuildError {
339    MissingField(String),
340}
341
342impl StdError for ProductListBuildError {
343    fn description(&self) -> &str {
344        match *self {
345            ProductListBuildError::MissingField(ref msg) => msg,
346        }
347    }
348
349    fn source(&self) -> Option<&(dyn StdError + 'static)> {
350        match *self {
351            ProductListBuildError::MissingField(_) => None,
352        }
353    }
354}
355
356impl std::fmt::Display for ProductListBuildError {
357    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
358        match *self {
359            ProductListBuildError::MissingField(ref s) => write!(f, "missing field \"{}\"", s),
360        }
361    }
362}
363
364/// Builder used to create a `ProductList`
365#[derive(Default, Clone)]
366pub struct ProductListBuilder {
367    pub products: Option<Vec<Product>>,
368}
369
370impl ProductListBuilder {
371    pub fn new() -> Self {
372        ProductListBuilder::default()
373    }
374
375    pub fn with_products(mut self, products: Vec<Product>) -> ProductListBuilder {
376        self.products = Some(products);
377        self
378    }
379
380    pub fn build(self) -> Result<ProductList, ProductListBuildError> {
381        // Product values are not required
382        let products = self.products.ok_or_else(|| {
383            ProductListBuildError::MissingField("'products' field is required".to_string())
384        })?;
385
386        let products = {
387            if products.is_empty() {
388                return Err(ProductListBuildError::MissingField(
389                    "'products' cannot be empty".to_string(),
390                ));
391            } else {
392                products
393            }
394        };
395
396        Ok(ProductList { products })
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::protocol::schema::state::{DataType, PropertyValueBuilder};
404    use std::fmt::Debug;
405
406    #[test]
407    /// Validate that a `Product` may be built correctly
408    fn test_product_builder() {
409        let product = build_product();
410
411        assert_eq!(product.product_id(), "688955434684");
412        assert_eq!(*product.product_namespace(), ProductNamespace::Gs1);
413        assert_eq!(product.owner(), "Target");
414        assert_eq!(product.properties()[0].name(), "description");
415        assert_eq!(*product.properties()[0].data_type(), DataType::String);
416        assert_eq!(
417            product.properties()[0].string_value(),
418            "This is a product description"
419        );
420        assert_eq!(product.properties()[1].name(), "price");
421        assert_eq!(*product.properties()[1].data_type(), DataType::Number);
422        assert_eq!(*product.properties()[1].number_value(), 3);
423    }
424
425    #[test]
426    /// Validate that a `Product` may be correctly converted back to its respective builder
427    fn test_product_into_builder() {
428        let product = build_product();
429
430        let builder = product.into_builder();
431
432        assert_eq!(builder.product_id, Some("688955434684".to_string()));
433        assert_eq!(builder.product_namespace, Some(ProductNamespace::Gs1));
434        assert_eq!(builder.owner, Some("Target".to_string()));
435        assert_eq!(builder.properties, Some(make_properties()));
436    }
437
438    #[test]
439    /// Validate that a `Product` may be correctly converted into bytes and then back to its native
440    /// representation
441    fn test_product_into_bytes() {
442        let builder = ProductBuilder::new();
443        let original = builder
444            .with_product_id("688955434684".into())
445            .with_product_namespace(ProductNamespace::Gs1)
446            .with_owner("Target".into())
447            .with_properties(make_properties())
448            .build()
449            .unwrap();
450
451        test_from_bytes(original, Product::from_bytes);
452    }
453
454    #[test]
455    /// Validate that a list of products, `ProductList`, can be built correctly
456    fn test_product_list_builder() {
457        let product_list = build_product_list();
458
459        assert_eq!(product_list.products.len(), 2);
460
461        // Test product 1
462        assert_eq!(product_list.products[0].product_id(), "688955434684");
463        assert_eq!(
464            *product_list.products[0].product_namespace(),
465            ProductNamespace::Gs1
466        );
467        assert_eq!(product_list.products[0].owner(), "Target");
468        assert_eq!(
469            product_list.products[0].properties()[0].name(),
470            "description"
471        );
472        assert_eq!(
473            *product_list.products[0].properties()[0].data_type(),
474            DataType::String
475        );
476        assert_eq!(
477            product_list.products[0].properties()[0].string_value(),
478            "This is a product description"
479        );
480        assert_eq!(product_list.products[0].properties()[1].name(), "price");
481        assert_eq!(
482            *product_list.products[0].properties()[1].data_type(),
483            DataType::Number
484        );
485        assert_eq!(*product_list.products[0].properties()[1].number_value(), 3);
486
487        // Test product 2
488        assert_eq!(product_list.products[1].product_id(), "688955434685");
489        assert_eq!(
490            *product_list.products[1].product_namespace(),
491            ProductNamespace::Gs1
492        );
493        assert_eq!(product_list.products[1].owner(), "Cargill");
494        assert_eq!(
495            product_list.products[1].properties()[0].name(),
496            "description"
497        );
498        assert_eq!(
499            *product_list.products[1].properties()[0].data_type(),
500            DataType::String
501        );
502        assert_eq!(
503            product_list.products[1].properties()[0].string_value(),
504            "This is a product description"
505        );
506        assert_eq!(product_list.products[1].properties()[1].name(), "price");
507        assert_eq!(
508            *product_list.products[1].properties()[1].data_type(),
509            DataType::Number
510        );
511        assert_eq!(*product_list.products[1].properties()[1].number_value(), 3);
512    }
513
514    #[test]
515    /// Validate that a `ProductList` can be correctly converted back to a builder
516    fn test_product_list_into_builder() {
517        let product_list = build_product_list();
518
519        let builder = product_list.into_builder();
520
521        assert_eq!(builder.products, Some(make_products()));
522    }
523
524    #[test]
525    /// Validate that a `ProductList` can be converted into bytes and back to its native
526    /// representation successfully
527    fn test_product_list_into_bytes() {
528        let builder = ProductListBuilder::new();
529        let original = builder.with_products(make_products()).build().unwrap();
530
531        test_from_bytes(original, ProductList::from_bytes);
532    }
533
534    fn build_product() -> Product {
535        ProductBuilder::new()
536            .with_product_id("688955434684".into()) // GTIN-12
537            .with_product_namespace(ProductNamespace::Gs1)
538            .with_owner("Target".into())
539            .with_properties(make_properties())
540            .build()
541            .expect("Failed to build test product")
542    }
543
544    fn make_properties() -> Vec<PropertyValue> {
545        let property_value_description = PropertyValueBuilder::new()
546            .with_name("description".into())
547            .with_data_type(DataType::String)
548            .with_string_value("This is a product description".into())
549            .build()
550            .unwrap();
551        let property_value_price = PropertyValueBuilder::new()
552            .with_name("price".into())
553            .with_data_type(DataType::Number)
554            .with_number_value(3)
555            .build()
556            .unwrap();
557
558        vec![
559            property_value_description.clone(),
560            property_value_price.clone(),
561        ]
562    }
563
564    fn build_product_list() -> ProductList {
565        ProductListBuilder::new()
566            .with_products(make_products())
567            .build()
568            .expect("Failed to build test product list")
569    }
570
571    fn make_products() -> Vec<Product> {
572        vec![
573            ProductBuilder::new()
574                .with_product_id("688955434684".into()) // GTIN-12
575                .with_product_namespace(ProductNamespace::Gs1)
576                .with_owner("Target".into())
577                .with_properties(make_properties())
578                .build()
579                .expect("Failed to build test product"),
580            ProductBuilder::new()
581                .with_product_id("688955434685".into()) // GTIN-12
582                .with_product_namespace(ProductNamespace::Gs1)
583                .with_owner("Cargill".into())
584                .with_properties(make_properties())
585                .build()
586                .expect("Failed to build test product"),
587        ]
588    }
589
590    fn test_from_bytes<T: FromBytes<T> + Clone + PartialEq + IntoBytes + Debug, F>(
591        under_test: T,
592        from_bytes: F,
593    ) where
594        F: Fn(&[u8]) -> Result<T, ProtoConversionError>,
595    {
596        let bytes = under_test.clone().into_bytes().unwrap();
597        let created_from_bytes = from_bytes(&bytes).unwrap();
598        assert_eq!(under_test, created_from_bytes);
599    }
600}