From 8805dcf85de26ed308a7982257130442ea8ffa15 Mon Sep 17 00:00:00 2001 From: Raphael Santo Domingo Date: Tue, 9 Jul 2019 16:42:49 -0500 Subject: [PATCH] Add rust native implementation of protobufs Other changes - Removed enum ProductType declarations in each Product action message (see protos/product_payload.proto) - Renamed 'product_values' field to 'properties' field for coherency (see protos/product_state.proto) - Replaced deprecated StdError 'cause' function with new 'source' function (see protocol/product/state.rs) Signed-off-by: Raphael Santo Domingo --- sdk/protos/product_payload.proto | 20 +- sdk/protos/product_state.proto | 4 +- sdk/src/protocol/mod.rs | 1 + sdk/src/protocol/product/mod.rs | 18 + sdk/src/protocol/product/payload.rs | 742 ++++++++++++++++++++++++++++ sdk/src/protocol/product/state.rs | 570 +++++++++++++++++++++ 6 files changed, 1337 insertions(+), 18 deletions(-) create mode 100644 sdk/src/protocol/product/mod.rs create mode 100644 sdk/src/protocol/product/payload.rs create mode 100644 sdk/src/protocol/product/state.rs diff --git a/sdk/protos/product_payload.proto b/sdk/protos/product_payload.proto index abcfb7f949..f308fd7423 100644 --- a/sdk/protos/product_payload.proto +++ b/sdk/protos/product_payload.proto @@ -37,36 +37,24 @@ message ProductPayload { } message ProductCreateAction { - enum ProductType { - UNSET_TYPE = 0; - GS1 = 1; - } // product_type and identifier are used in deriving the state address - ProductType product_type = 1; + Product.ProductType product_type = 1; string identifier = 2; string owner = 3; repeated PropertyValue properties = 4; } message ProductUpdateAction { - enum ProductType { - UNSET_TYPE = 0; - GS1 = 1; - } // product_type and identifier are used in deriving the state address - ProductType product_type = 1; + Product.ProductType product_type = 1; string identifier = 2; // this will replace all properties currently defined - repeated PropertyValue properties = 4; + repeated PropertyValue properties = 3; } message ProductDeleteAction { - enum ProductType { - UNSET_TYPE = 0; - GS1 = 1; - } // product_type and identifier are used in deriving the state address - ProductType product_type = 1; + Product.ProductType product_type = 1; string identifier = 2; } \ No newline at end of file diff --git a/sdk/protos/product_state.proto b/sdk/protos/product_state.proto index d5983c8c64..d8ced4cb32 100644 --- a/sdk/protos/product_state.proto +++ b/sdk/protos/product_state.proto @@ -26,13 +26,13 @@ message Product { string identifier = 1; // What type of product is this (GS1) - ProductType type = 2; + ProductType product_type = 2; // Who owns this product (pike organization id) string owner = 3; // Addition attributes for custom configurations - repeated PropertyValue product_values = 4; + repeated PropertyValue properties = 4; } message ProductList { diff --git a/sdk/src/protocol/mod.rs b/sdk/src/protocol/mod.rs index e5ba2b9120..4021c1b0e5 100644 --- a/sdk/src/protocol/mod.rs +++ b/sdk/src/protocol/mod.rs @@ -14,5 +14,6 @@ pub mod errors; pub mod pike; +pub mod product; pub mod schema; pub mod track_and_trace; diff --git a/sdk/src/protocol/product/mod.rs b/sdk/src/protocol/product/mod.rs new file mode 100644 index 0000000000..a030677b32 --- /dev/null +++ b/sdk/src/protocol/product/mod.rs @@ -0,0 +1,18 @@ +// Copyright (c) 2019 Target Brands, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::errors; + +pub mod payload; +pub mod state; diff --git a/sdk/src/protocol/product/payload.rs b/sdk/src/protocol/product/payload.rs new file mode 100644 index 0000000000..59580794b5 --- /dev/null +++ b/sdk/src/protocol/product/payload.rs @@ -0,0 +1,742 @@ +// Copyright (c) 2019 Target Brands, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use protobuf::Message; +use protobuf::RepeatedField; + +use std::error::Error as StdError; + +use super::errors::BuilderError; + +use crate::protocol::{product::state::ProductType, schema::state::PropertyValue}; +use crate::protos; +use crate::protos::{product_payload, product_payload::ProductPayload_Action}; +use crate::protos::{ + FromBytes, FromNative, FromProto, IntoBytes, IntoNative, IntoProto, ProtoConversionError, +}; + +/// Native implementation for ProductPayload_Action +#[derive(Debug, Clone, PartialEq)] +pub enum Action { + ProductCreate(ProductCreateAction), + ProductUpdate(ProductUpdateAction), + ProductDelete(ProductDeleteAction), +} + +// Rust native implementation for ProductPayload +#[derive(Debug, Clone, PartialEq)] +pub struct ProductPayload { + action: Action, + timestamp: u64, +} + +impl ProductPayload { + pub fn action(&self) -> &Action { + &self.action + } + pub fn timestamp(&self) -> &u64 { + &self.timestamp + } +} + +impl FromProto for ProductPayload { + fn from_proto( + payload: protos::product_payload::ProductPayload, + ) -> Result { + let action = match payload.get_action() { + ProductPayload_Action::PRODUCT_CREATE => Action::ProductCreate( + ProductCreateAction::from_proto(payload.get_product_create().clone())?, + ), + ProductPayload_Action::PRODUCT_UPDATE => Action::ProductUpdate( + ProductUpdateAction::from_proto(payload.get_product_update().clone())?, + ), + ProductPayload_Action::PRODUCT_DELETE => Action::ProductDelete( + ProductDeleteAction::from_proto(payload.get_product_delete().clone())?, + ), + ProductPayload_Action::UNSET_ACTION => { + return Err(ProtoConversionError::InvalidTypeError( + "Cannot convert ProductPayload_Action with type unset".to_string(), + )); + } + }; + Ok(ProductPayload { + action, + timestamp: payload.get_timestamp(), + }) + } +} + +impl FromNative for protos::product_payload::ProductPayload { + fn from_native(native: ProductPayload) -> Result { + let mut proto = product_payload::ProductPayload::new(); + + proto.set_timestamp(*native.timestamp()); + + match native.action() { + Action::ProductCreate(payload) => { + proto.set_action(ProductPayload_Action::PRODUCT_CREATE); + proto.set_product_create(payload.clone().into_proto()?); + } + Action::ProductUpdate(payload) => { + proto.set_action(ProductPayload_Action::PRODUCT_UPDATE); + proto.set_product_update(payload.clone().into_proto()?); + } + Action::ProductDelete(payload) => { + proto.set_action(ProductPayload_Action::PRODUCT_DELETE); + proto.set_product_delete(payload.clone().into_proto()?); + } + } + + Ok(proto) + } +} + +impl FromBytes for ProductPayload { + fn from_bytes(bytes: &[u8]) -> Result { + let proto: product_payload::ProductPayload = + protobuf::parse_from_bytes(bytes).map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get ProductPayload from bytes".into(), + ) + })?; + proto.into_native() + } +} + +impl IntoBytes for ProductPayload { + fn into_bytes(self) -> Result, ProtoConversionError> { + let proto = self.into_proto()?; + let bytes = proto.write_to_bytes().map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get ProductPayload from bytes".into(), + ) + })?; + Ok(bytes) + } +} + +impl IntoProto for ProductPayload {} +impl IntoNative for protos::product_payload::ProductPayload {} + +#[derive(Debug)] +pub enum ProductPayloadBuildError { + MissingField(String), +} + +impl StdError for ProductPayloadBuildError { + fn description(&self) -> &str { + match *self { + ProductPayloadBuildError::MissingField(ref msg) => msg, + } + } + + fn cause(&self) -> Option<&StdError> { + match *self { + ProductPayloadBuildError::MissingField(_) => None, + } + } +} + +impl std::fmt::Display for ProductPayloadBuildError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + ProductPayloadBuildError::MissingField(ref s) => write!(f, "missing field \"{}\"", s), + } + } +} + +/// Builder used to create a ProductPayload +#[derive(Default, Clone)] +pub struct ProductPayloadBuilder { + action: Option, + timestamp: Option, +} + +impl ProductPayloadBuilder { + pub fn new() -> Self { + ProductPayloadBuilder::default() + } + pub fn with_action(mut self, action: Action) -> ProductPayloadBuilder { + self.action = Some(action); + self + } + pub fn with_timestamp(mut self, value: u64) -> Self { + self.timestamp = Some(value); + self + } + pub fn build(self) -> Result { + let action = self + .action + .ok_or_else(|| BuilderError::MissingField("'action' field is required".into()))?; + let timestamp = self + .timestamp + .ok_or_else(|| BuilderError::MissingField("'timestamp' field is required".into()))?; + Ok(ProductPayload { action, timestamp }) + } +} + +/// Native implementation for ProductCreateAction +#[derive(Debug, Clone, PartialEq)] +pub struct ProductCreateAction { + product_type: ProductType, + identifier: String, + owner: String, + properties: Vec, +} + +impl ProductCreateAction { + pub fn product_type(&self) -> &ProductType { + &self.product_type + } + + pub fn identifier(&self) -> &str { + &self.identifier + } + + pub fn owner(&self) -> &str { + &self.owner + } + + pub fn properties(&self) -> &[PropertyValue] { + &self.properties + } +} + +impl FromProto for ProductCreateAction { + fn from_proto( + proto: product_payload::ProductCreateAction, + ) -> Result { + Ok(ProductCreateAction { + product_type: ProductType::from_proto(proto.get_product_type())?, + identifier: proto.get_identifier().to_string(), + owner: proto.get_owner().to_string(), + properties: proto + .get_properties() + .to_vec() + .into_iter() + .map(PropertyValue::from_proto) + .collect::, ProtoConversionError>>()?, + }) + } +} + +impl FromNative for product_payload::ProductCreateAction { + fn from_native(native: ProductCreateAction) -> Result { + let mut proto = protos::product_payload::ProductCreateAction::new(); + proto.set_product_type(native.product_type().clone().into_proto()?); + proto.set_identifier(native.identifier().to_string()); + proto.set_owner(native.owner().to_string()); + proto.set_properties(RepeatedField::from_vec( + native + .properties() + .to_vec() + .into_iter() + .map(PropertyValue::into_proto) + .collect::, ProtoConversionError>>( + )?, + )); + Ok(proto) + } +} + +impl FromBytes for ProductCreateAction { + fn from_bytes(bytes: &[u8]) -> Result { + let proto: protos::product_payload::ProductCreateAction = protobuf::parse_from_bytes(bytes) + .map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get ProductCreateAction from bytes".to_string(), + ) + })?; + proto.into_native() + } +} + +impl IntoBytes for ProductCreateAction { + fn into_bytes(self) -> Result, ProtoConversionError> { + let proto = self.into_proto()?; + let bytes = proto.write_to_bytes().map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get bytes from ProductCreateAction".to_string(), + ) + })?; + Ok(bytes) + } +} + +impl IntoProto for ProductCreateAction {} +impl IntoNative for protos::product_payload::ProductCreateAction {} + +#[derive(Default, Debug)] +pub struct ProductCreateActionBuilder { + product_type: Option, + identifier: Option, + owner: Option, + properties: Option>, +} + +impl ProductCreateActionBuilder { + pub fn new() -> Self { + ProductCreateActionBuilder::default() + } + pub fn with_product_type(mut self, value: ProductType) -> Self { + self.product_type = Some(value); + self + } + pub fn with_identifier(mut self, value: String) -> Self { + self.identifier = Some(value); + self + } + pub fn with_owner(mut self, value: String) -> Self { + self.owner = Some(value); + self + } + pub fn with_properties(mut self, value: Vec) -> Self { + self.properties = Some(value); + self + } + pub fn build(self) -> Result { + let product_type = self.product_type.ok_or_else(|| { + BuilderError::MissingField("'product_type' field is required".to_string()) + })?; + let identifier = self + .identifier + .ok_or_else(|| BuilderError::MissingField("'identifier' field is required".into()))?; + let owner = self + .owner + .ok_or_else(|| BuilderError::MissingField("'owner' field is required".into()))?; + let properties = self + .properties + .ok_or_else(|| BuilderError::MissingField("'properties' field is required".into()))?; + Ok(ProductCreateAction { + product_type, + identifier, + owner, + properties, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProductUpdateAction { + product_type: ProductType, + identifier: String, + properties: Vec, +} + +/// Native implementation for ProductUpdateAction +impl ProductUpdateAction { + pub fn product_type(&self) -> &ProductType { + &self.product_type + } + + pub fn identifier(&self) -> &str { + &self.identifier + } + + pub fn properties(&self) -> &[PropertyValue] { + &self.properties + } +} + +impl FromProto for ProductUpdateAction { + fn from_proto( + proto: protos::product_payload::ProductUpdateAction, + ) -> Result { + Ok(ProductUpdateAction { + product_type: ProductType::from_proto(proto.get_product_type())?, + identifier: proto.get_identifier().to_string(), + properties: proto + .get_properties() + .to_vec() + .into_iter() + .map(PropertyValue::from_proto) + .collect::, ProtoConversionError>>()?, + }) + } +} + +impl FromNative for protos::product_payload::ProductUpdateAction { + fn from_native(native: ProductUpdateAction) -> Result { + let mut proto = protos::product_payload::ProductUpdateAction::new(); + proto.set_product_type(native.product_type().clone().into_proto()?); + proto.set_identifier(native.identifier().to_string()); + proto.set_properties(RepeatedField::from_vec( + native + .properties() + .to_vec() + .into_iter() + .map(PropertyValue::into_proto) + .collect::, ProtoConversionError>>( + )?, + )); + + Ok(proto) + } +} + +impl FromBytes for ProductUpdateAction { + fn from_bytes(bytes: &[u8]) -> Result { + let proto: protos::product_payload::ProductUpdateAction = protobuf::parse_from_bytes(bytes) + .map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get ProductUpdateAction from bytes".to_string(), + ) + })?; + proto.into_native() + } +} + +impl IntoBytes for ProductUpdateAction { + fn into_bytes(self) -> Result, ProtoConversionError> { + let proto = self.into_proto()?; + let bytes = proto.write_to_bytes().map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get bytes from ProductUpdateAction".to_string(), + ) + })?; + Ok(bytes) + } +} + +impl IntoProto for ProductUpdateAction {} +impl IntoNative for protos::product_payload::ProductUpdateAction {} + +/// Builder used to create a ProductUpdateActionBuilder +#[derive(Default, Clone)] +pub struct ProductUpdateActionBuilder { + product_type: Option, + identifier: Option, + properties: Vec, +} + +impl ProductUpdateActionBuilder { + pub fn new() -> Self { + ProductUpdateActionBuilder::default() + } + + pub fn with_product_type(mut self, product_type: ProductType) -> Self { + self.product_type = Some(product_type); + self + } + + pub fn with_identifier(mut self, identifier: String) -> Self { + self.identifier = Some(identifier); + self + } + + pub fn with_properties(mut self, properties: Vec) -> Self { + self.properties = properties; + self + } + + pub fn build(self) -> Result { + let product_type = self.product_type.ok_or_else(|| { + BuilderError::MissingField("'product_type' field is required".to_string()) + })?; + + let identifier = self.identifier.ok_or_else(|| { + BuilderError::MissingField("'identifier' field is required".to_string()) + })?; + + let properties = { + if !self.properties.is_empty() { + self.properties + } else { + return Err(BuilderError::MissingField( + "'properties' field is required".to_string(), + )); + } + }; + + Ok(ProductUpdateAction { + product_type, + identifier, + properties, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProductDeleteAction { + product_type: ProductType, + identifier: String, +} + +/// Native implementation for ProductDeleteAction +impl ProductDeleteAction { + pub fn product_type(&self) -> &ProductType { + &self.product_type + } + + pub fn identifier(&self) -> &str { + &self.identifier + } +} + +impl FromProto for ProductDeleteAction { + fn from_proto( + proto: protos::product_payload::ProductDeleteAction, + ) -> Result { + Ok(ProductDeleteAction { + product_type: ProductType::from_proto(proto.get_product_type())?, + identifier: proto.get_identifier().to_string(), + }) + } +} + +impl FromNative for protos::product_payload::ProductDeleteAction { + fn from_native(native: ProductDeleteAction) -> Result { + let mut proto = protos::product_payload::ProductDeleteAction::new(); + proto.set_product_type(native.product_type().clone().into_proto()?); + proto.set_identifier(native.identifier().to_string()); + Ok(proto) + } +} + +impl FromBytes for ProductDeleteAction { + fn from_bytes(bytes: &[u8]) -> Result { + let proto: protos::product_payload::ProductDeleteAction = protobuf::parse_from_bytes(bytes) + .map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get ProductDeleteAction from bytes".to_string(), + ) + })?; + proto.into_native() + } +} + +impl IntoBytes for ProductDeleteAction { + fn into_bytes(self) -> Result, ProtoConversionError> { + let proto = self.into_proto()?; + let bytes = proto.write_to_bytes().map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get bytes from ProductDeleteAction".to_string(), + ) + })?; + Ok(bytes) + } +} + +impl IntoProto for ProductDeleteAction {} +impl IntoNative for protos::product_payload::ProductDeleteAction {} + +/// Builder used to create a ProductDeleteActionBuilder +#[derive(Default, Clone)] +pub struct ProductDeleteActionBuilder { + product_type: Option, + identifier: Option, +} + +impl ProductDeleteActionBuilder { + pub fn new() -> Self { + ProductDeleteActionBuilder::default() + } + + pub fn with_product_type(mut self, product_type: ProductType) -> Self { + self.product_type = Some(product_type); + self + } + + pub fn with_identifier(mut self, identifier: String) -> Self { + self.identifier = Some(identifier); + self + } + + pub fn build(self) -> Result { + let product_type = self.product_type.ok_or_else(|| { + BuilderError::MissingField("'product_type' field is required".to_string()) + })?; + + let identifier = self.identifier.ok_or_else(|| { + BuilderError::MissingField("'identifier' field is required".to_string()) + })?; + + Ok(ProductDeleteAction { + product_type, + identifier, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::schema::state::{DataType, PropertyValueBuilder}; + use std::fmt::Debug; + + #[test] + fn test_product_create_builder() { + let action = ProductCreateActionBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_owner("Target".into()) + .with_properties(make_properties()) + .build() + .unwrap(); + + assert_eq!(action.identifier(), "688955434684"); + assert_eq!(action.owner(), "Target"); + assert_eq!(*action.product_type(), ProductType::GS1); + assert_eq!(action.properties()[0].name(), "description"); + assert_eq!(*action.properties()[0].data_type(), DataType::String); + assert_eq!( + action.properties()[0].string_value(), + "This is a product description" + ); + assert_eq!(action.properties()[1].name(), "price"); + assert_eq!(*action.properties()[1].data_type(), DataType::Number); + assert_eq!(*action.properties()[1].number_value(), 3); + } + + #[test] + fn test_product_create_into_bytes() { + let action = ProductCreateActionBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_owner("Target".into()) + .with_properties(make_properties()) + .build() + .unwrap(); + + test_from_bytes(action, ProductCreateAction::from_bytes); + } + + #[test] + fn test_product_update_builder() { + let action = ProductUpdateActionBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_properties(make_properties()) + .build() + .unwrap(); + + assert_eq!(action.identifier(), "688955434684"); + assert_eq!(*action.product_type(), ProductType::GS1); + assert_eq!(action.properties()[0].name(), "description"); + assert_eq!(*action.properties()[0].data_type(), DataType::String); + assert_eq!( + action.properties()[0].string_value(), + "This is a product description" + ); + assert_eq!(action.properties()[1].name(), "price"); + assert_eq!(*action.properties()[1].data_type(), DataType::Number); + assert_eq!(*action.properties()[1].number_value(), 3); + } + + #[test] + fn test_product_update_into_bytes() { + let action = ProductUpdateActionBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_properties(make_properties()) + .build() + .unwrap(); + + test_from_bytes(action, ProductUpdateAction::from_bytes); + } + + #[test] + fn test_product_delete_builder() { + let action = ProductDeleteActionBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .build() + .unwrap(); + + assert_eq!(action.identifier(), "688955434684"); + assert_eq!(*action.product_type(), ProductType::GS1); + } + + #[test] + fn test_product_delete_into_bytes() { + let action = ProductDeleteActionBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .build() + .unwrap(); + + test_from_bytes(action, ProductDeleteAction::from_bytes); + } + + fn make_properties() -> Vec { + let property_value_description = PropertyValueBuilder::new() + .with_name("description".into()) + .with_data_type(DataType::String) + .with_string_value("This is a product description".into()) + .build() + .unwrap(); + let property_value_price = PropertyValueBuilder::new() + .with_name("price".into()) + .with_data_type(DataType::Number) + .with_number_value(3) + .build() + .unwrap(); + + vec![ + property_value_description.clone(), + property_value_price.clone(), + ] + } + + #[test] + fn test_product_payload_builder() { + let action = ProductCreateActionBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_owner("Target".into()) + .with_properties(make_properties()) + .build() + .unwrap(); + + let payload = ProductPayloadBuilder::new() + .with_action(Action::ProductCreate(action.clone())) + .with_timestamp(0) + .build() + .unwrap(); + + assert_eq!(*payload.action(), Action::ProductCreate(action)); + assert_eq!(*payload.timestamp(), 0); + } + + #[test] + fn test_product_payload_bytes() { + let action = ProductCreateActionBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_owner("Target".into()) + .with_properties(make_properties()) + .build() + .unwrap(); + + let payload = ProductPayloadBuilder::new() + .with_action(Action::ProductCreate(action.clone())) + .with_timestamp(0) + .build() + .unwrap(); + + test_from_bytes(payload, ProductPayload::from_bytes); + } + + fn test_from_bytes + Clone + PartialEq + IntoBytes + Debug, F>( + under_test: T, + from_bytes: F, + ) where + F: Fn(&[u8]) -> Result, + { + let bytes = under_test.clone().into_bytes().unwrap(); + let created_from_bytes = from_bytes(&bytes).unwrap(); + assert_eq!(under_test, created_from_bytes); + } + +} diff --git a/sdk/src/protocol/product/state.rs b/sdk/src/protocol/product/state.rs new file mode 100644 index 0000000000..a04d61aba8 --- /dev/null +++ b/sdk/src/protocol/product/state.rs @@ -0,0 +1,570 @@ +// Copyright (c) 2019 Target Brands, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use protobuf::Message; +use protobuf::RepeatedField; + +use std::error::Error as StdError; + +use crate::protos; +use crate::protos::schema_state; +use crate::protos::{ + FromBytes, FromNative, FromProto, IntoBytes, IntoNative, IntoProto, ProtoConversionError, +}; + +use crate::protocol::schema::state::PropertyValue; + +/// Native implementation of ProductType enum +#[derive(Debug, Clone, PartialEq)] +pub enum ProductType { + GS1, +} + +impl FromProto for ProductType { + fn from_proto( + product_type: protos::product_state::Product_ProductType, + ) -> Result { + match product_type { + protos::product_state::Product_ProductType::GS1 => Ok(ProductType::GS1), + protos::product_state::Product_ProductType::UNSET_TYPE => { + Err(ProtoConversionError::InvalidTypeError( + "Cannot convert Product_ProductType with type UNSET_TYPE".to_string(), + )) + } + } + } +} + +impl FromNative for protos::product_state::Product_ProductType { + fn from_native(product_type: ProductType) -> Result { + match product_type { + ProductType::GS1 => Ok(protos::product_state::Product_ProductType::GS1), + } + } +} + +impl IntoProto for ProductType {} +impl IntoNative for protos::product_state::Product_ProductType {} + +/// Native implementation of Product +#[derive(Debug, Clone, PartialEq)] +pub struct Product { + identifier: String, + product_type: ProductType, + owner: String, + properties: Vec, +} + +impl Product { + pub fn identifier(&self) -> &str { + &self.identifier + } + + pub fn product_type(&self) -> &ProductType { + &self.product_type + } + + pub fn owner(&self) -> &str { + &self.owner + } + + pub fn properties(&self) -> &[PropertyValue] { + &self.properties + } + + pub fn into_builder(self) -> ProductBuilder { + ProductBuilder::new() + .with_identifier(self.identifier) + .with_product_type(self.product_type) + .with_owner(self.owner) + .with_properties(self.properties) + } +} + +impl FromProto for Product { + fn from_proto(product: protos::product_state::Product) -> Result { + Ok(Product { + identifier: product.get_identifier().to_string(), + product_type: ProductType::from_proto(product.get_product_type())?, + owner: product.get_owner().to_string(), + properties: product + .get_properties() + .to_vec() + .into_iter() + .map(PropertyValue::from_proto) + .collect::, ProtoConversionError>>()?, + }) + } +} + +impl FromNative for protos::product_state::Product { + fn from_native(product: Product) -> Result { + let mut proto = protos::product_state::Product::new(); + proto.set_identifier(product.identifier().to_string()); + proto.set_product_type(product.product_type().clone().into_proto()?); + proto.set_owner(product.owner().to_string()); + proto.set_properties(RepeatedField::from_vec( + product + .properties() + .to_vec() + .into_iter() + .map(PropertyValue::into_proto) + .collect::, ProtoConversionError>>()?, + )); + Ok(proto) + } +} + +impl FromBytes for Product { + fn from_bytes(bytes: &[u8]) -> Result { + let proto: protos::product_state::Product = + protobuf::parse_from_bytes(bytes).map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get Product from bytes".to_string(), + ) + })?; + proto.into_native() + } +} + +impl IntoBytes for Product { + fn into_bytes(self) -> Result, ProtoConversionError> { + let proto = self.into_proto()?; + let bytes = proto.write_to_bytes().map_err(|_| { + ProtoConversionError::SerializationError("Unable to get bytes from Product".to_string()) + })?; + Ok(bytes) + } +} + +impl IntoProto for Product {} +impl IntoNative for protos::product_state::Product {} + +#[derive(Debug)] +pub enum ProductBuildError { + MissingField(String), + EmptyVec(String), +} + +impl StdError for ProductBuildError { + fn description(&self) -> &str { + match *self { + ProductBuildError::MissingField(ref msg) => msg, + ProductBuildError::EmptyVec(ref msg) => msg, + } + } + + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match *self { + ProductBuildError::MissingField(_) => None, + ProductBuildError::EmptyVec(_) => None, + } + } +} + +impl std::fmt::Display for ProductBuildError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + ProductBuildError::MissingField(ref s) => write!(f, "missing field \"{}\"", s), + ProductBuildError::EmptyVec(ref s) => write!(f, "\"{}\" must not be empty", s), + } + } +} + +/// Builder used to create a Product +#[derive(Default, Clone, PartialEq)] +pub struct ProductBuilder { + pub identifier: Option, + pub product_type: Option, + pub owner: Option, + pub properties: Option>, +} + +impl ProductBuilder { + pub fn new() -> Self { + ProductBuilder::default() + } + + pub fn with_identifier(mut self, identifier: String) -> Self { + self.identifier = Some(identifier); + self + } + + pub fn with_product_type(mut self, product_type: ProductType) -> Self { + self.product_type = Some(product_type); + self + } + + pub fn with_owner(mut self, owner: String) -> Self { + self.owner = Some(owner); + self + } + + pub fn with_properties(mut self, properties: Vec) -> Self { + self.properties = Some(properties); + self + } + + pub fn build(self) -> Result { + let identifier = self.identifier.ok_or_else(|| { + ProductBuildError::MissingField("'identifier' field is required".to_string()) + })?; + + let product_type = self.product_type.ok_or_else(|| { + ProductBuildError::MissingField("'product_type' field is required".to_string()) + })?; + + let owner = self.owner.ok_or_else(|| { + ProductBuildError::MissingField("'owner' field is required".to_string()) + })?; + + // Product values are not required + let properties = self.properties.ok_or_else(|| { + ProductBuildError::MissingField("'properties' field is required".to_string()) + })?; + + Ok(Product { + identifier, + product_type, + owner, + properties, + }) + } +} + +/// Native implementation of ProductList +#[derive(Debug, Clone, PartialEq)] +pub struct ProductList { + products: Vec, +} + +impl ProductList { + pub fn products(&self) -> &[Product] { + &self.products + } + + pub fn into_builder(self) -> ProductListBuilder { + ProductListBuilder::new().with_products(self.products) + } +} + +impl FromProto for ProductList { + fn from_proto( + product_list: protos::product_state::ProductList, + ) -> Result { + Ok(ProductList { + products: product_list + .get_entries() + .to_vec() + .into_iter() + .map(Product::from_proto) + .collect::, ProtoConversionError>>()?, + }) + } +} + +impl FromNative for protos::product_state::ProductList { + fn from_native(product_list: ProductList) -> Result { + let mut product_list_proto = protos::product_state::ProductList::new(); + + product_list_proto.set_entries(RepeatedField::from_vec( + product_list + .products() + .to_vec() + .into_iter() + .map(Product::into_proto) + .collect::, ProtoConversionError>>()?, + )); + + Ok(product_list_proto) + } +} + +impl FromBytes for ProductList { + fn from_bytes(bytes: &[u8]) -> Result { + let proto: protos::product_state::ProductList = + protobuf::parse_from_bytes(bytes).map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get ProductList from bytes".to_string(), + ) + })?; + proto.into_native() + } +} + +impl IntoBytes for ProductList { + fn into_bytes(self) -> Result, ProtoConversionError> { + let proto = self.into_proto()?; + let bytes = proto.write_to_bytes().map_err(|_| { + ProtoConversionError::SerializationError( + "Unable to get bytes from ProductList".to_string(), + ) + })?; + Ok(bytes) + } +} + +impl IntoProto for ProductList {} +impl IntoNative for protos::product_state::ProductList {} + +#[derive(Debug)] +pub enum ProductListBuildError { + MissingField(String), +} + +impl StdError for ProductListBuildError { + fn description(&self) -> &str { + match *self { + ProductListBuildError::MissingField(ref msg) => msg, + } + } + + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match *self { + ProductListBuildError::MissingField(_) => None, + } + } +} + +impl std::fmt::Display for ProductListBuildError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + ProductListBuildError::MissingField(ref s) => write!(f, "missing field \"{}\"", s), + } + } +} + +/// Builder used to create a ProductList +#[derive(Default, Clone)] +pub struct ProductListBuilder { + pub products: Option>, +} + +impl ProductListBuilder { + pub fn new() -> Self { + ProductListBuilder::default() + } + + pub fn with_products(mut self, products: Vec) -> ProductListBuilder { + self.products = Some(products); + self + } + + pub fn build(self) -> Result { + // Product values are not required + let products = self.products.ok_or_else(|| { + ProductListBuildError::MissingField("'products' field is required".to_string()) + })?; + + let products = { + if products.is_empty() { + return Err(ProductListBuildError::MissingField( + "'products' cannot be empty".to_string(), + )); + } else { + products + } + }; + + Ok(ProductList { products }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::schema::state::{DataType, PropertyValueBuilder}; + use std::fmt::Debug; + + #[test] + fn test_product_builder() { + let product = build_product(); + + assert_eq!(product.identifier(), "688955434684"); + assert_eq!(*product.product_type(), ProductType::GS1); + assert_eq!(product.owner(), "Target"); + assert_eq!(product.properties()[0].name(), "description"); + assert_eq!(*product.properties()[0].data_type(), DataType::String); + assert_eq!( + product.properties()[0].string_value(), + "This is a product description" + ); + assert_eq!(product.properties()[1].name(), "price"); + assert_eq!(*product.properties()[1].data_type(), DataType::Number); + assert_eq!(*product.properties()[1].number_value(), 3); + } + + #[test] + fn test_product_into_builder() { + let product = build_product(); + + let builder = product.into_builder(); + + assert_eq!(builder.identifier, Some("688955434684".to_string())); + assert_eq!(builder.product_type, Some(ProductType::GS1)); + assert_eq!(builder.owner, Some("Target".to_string())); + assert_eq!(builder.properties, Some(make_properties())); + } + + #[test] + fn test_product_into_bytes() { + let builder = ProductBuilder::new(); + let original = builder + .with_identifier("688955434684".into()) + .with_product_type(ProductType::GS1) + .with_owner("Target".into()) + .with_properties(make_properties()) + .build() + .unwrap(); + + test_from_bytes(original, Product::from_bytes); + } + + #[test] + fn test_product_list_builder() { + let product_list = build_product_list(); + + assert_eq!(product_list.products.len(), 2); + + // Test product 1 + assert_eq!(product_list.products[0].identifier(), "688955434684"); + assert_eq!(*product_list.products[0].product_type(), ProductType::GS1); + assert_eq!(product_list.products[0].owner(), "Target"); + assert_eq!( + product_list.products[0].properties()[0].name(), + "description" + ); + assert_eq!( + *product_list.products[0].properties()[0].data_type(), + DataType::String + ); + assert_eq!( + product_list.products[0].properties()[0].string_value(), + "This is a product description" + ); + assert_eq!(product_list.products[0].properties()[1].name(), "price"); + assert_eq!( + *product_list.products[0].properties()[1].data_type(), + DataType::Number + ); + assert_eq!(*product_list.products[0].properties()[1].number_value(), 3); + + // Test product 2 + assert_eq!(product_list.products[1].identifier(), "688955434685"); + assert_eq!(*product_list.products[1].product_type(), ProductType::GS1); + assert_eq!(product_list.products[1].owner(), "Cargill"); + assert_eq!( + product_list.products[1].properties()[0].name(), + "description" + ); + assert_eq!( + *product_list.products[1].properties()[0].data_type(), + DataType::String + ); + assert_eq!( + product_list.products[1].properties()[0].string_value(), + "This is a product description" + ); + assert_eq!(product_list.products[1].properties()[1].name(), "price"); + assert_eq!( + *product_list.products[1].properties()[1].data_type(), + DataType::Number + ); + assert_eq!(*product_list.products[1].properties()[1].number_value(), 3); + } + + #[test] + fn test_product_list_into_builder() { + let product_list = build_product_list(); + + let builder = product_list.into_builder(); + + assert_eq!(builder.products, Some(make_products())); + } + + #[test] + fn test_product_list_into_bytes() { + let builder = ProductListBuilder::new(); + let original = builder.with_products(make_products()).build().unwrap(); + + test_from_bytes(original, ProductList::from_bytes); + } + + fn build_product() -> Product { + ProductBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_owner("Target".into()) + .with_properties(make_properties()) + .build() + .expect("Failed to build test product") + } + + fn make_properties() -> Vec { + let property_value_description = PropertyValueBuilder::new() + .with_name("description".into()) + .with_data_type(DataType::String) + .with_string_value("This is a product description".into()) + .build() + .unwrap(); + let property_value_price = PropertyValueBuilder::new() + .with_name("price".into()) + .with_data_type(DataType::Number) + .with_number_value(3) + .build() + .unwrap(); + + vec![ + property_value_description.clone(), + property_value_price.clone(), + ] + } + + fn build_product_list() -> ProductList { + ProductListBuilder::new() + .with_products(make_products()) + .build() + .expect("Failed to build test product list") + } + + fn make_products() -> Vec { + vec![ + ProductBuilder::new() + .with_identifier("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_owner("Target".into()) + .with_properties(make_properties()) + .build() + .expect("Failed to build test product"), + ProductBuilder::new() + .with_identifier("688955434685".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_owner("Cargill".into()) + .with_properties(make_properties()) + .build() + .expect("Failed to build test product"), + ] + } + + fn test_from_bytes + Clone + PartialEq + IntoBytes + Debug, F>( + under_test: T, + from_bytes: F, + ) where + F: Fn(&[u8]) -> Result, + { + let bytes = under_test.clone().into_bytes().unwrap(); + let created_from_bytes = from_bytes(&bytes).unwrap(); + assert_eq!(under_test, created_from_bytes); + } +}