From 9cea16f74a2878497cd2616e21b8e7c17210ef1e Mon Sep 17 00:00:00 2001 From: Raphael Santo Domingo Date: Tue, 9 Jul 2019 16:42:49 -0500 Subject: [PATCH 1/2] 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) - Renamed 'type' field to 'product_type' because 'type' is a reserved key word - Renamed 'identifier field to 'product_id' field because 'identifier' is a rust error descriptor - 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 | 32 +- sdk/protos/product_state.proto | 8 +- sdk/src/protocol/mod.rs | 1 + sdk/src/protocol/product/mod.rs | 18 + sdk/src/protocol/product/payload.rs | 750 ++++++++++++++++++++++++++++ sdk/src/protocol/product/state.rs | 576 +++++++++++++++++++++ 6 files changed, 1359 insertions(+), 26 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..60b5cd18f1 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; - string identifier = 2; + // product_type and product_id are used in deriving the state address + Product.ProductType product_type = 1; + string product_id = 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; - string identifier = 2; + // product_type and product_id are used in deriving the state address + Product.ProductType product_type = 1; + string product_id = 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; - string identifier = 2; + // product_type and product_id are used in deriving the state address + Product.ProductType product_type = 1; + string product_id = 2; } \ No newline at end of file diff --git a/sdk/protos/product_state.proto b/sdk/protos/product_state.proto index d5983c8c64..193f42aaa8 100644 --- a/sdk/protos/product_state.proto +++ b/sdk/protos/product_state.proto @@ -22,17 +22,17 @@ message Product { GS1 = 1; } - // Identifier for products (gtin) - string identifier = 1; + // product_id for products (gtin) + string product_id = 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..b158d4044e --- /dev/null +++ b/sdk/src/protocol/product/payload.rs @@ -0,0 +1,750 @@ +// 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, + product_id: String, + owner: String, + properties: Vec, +} + +impl ProductCreateAction { + pub fn product_type(&self) -> &ProductType { + &self.product_type + } + + pub fn product_id(&self) -> &str { + &self.product_id + } + + 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())?, + product_id: proto.get_product_id().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_product_id(native.product_id().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, + product_id: 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_product_id(mut self, value: String) -> Self { + self.product_id = 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 product_id = self + .product_id + .ok_or_else(|| BuilderError::MissingField("'product_id' 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, + product_id, + owner, + properties, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProductUpdateAction { + product_type: ProductType, + product_id: String, + properties: Vec, +} + +/// Native implementation for ProductUpdateAction +impl ProductUpdateAction { + pub fn product_type(&self) -> &ProductType { + &self.product_type + } + + pub fn product_id(&self) -> &str { + &self.product_id + } + + 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())?, + product_id: proto.get_product_id().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_product_id(native.product_id().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 ProductUpdateAction +#[derive(Default, Clone)] +pub struct ProductUpdateActionBuilder { + product_type: Option, + product_id: 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_product_id(mut self, product_id: String) -> Self { + self.product_id = Some(product_id); + 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 product_id = self.product_id.ok_or_else(|| { + BuilderError::MissingField("'product_id' 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, + product_id, + properties, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProductDeleteAction { + product_type: ProductType, + product_id: String, +} + +/// Native implementation for ProductDeleteAction +impl ProductDeleteAction { + pub fn product_type(&self) -> &ProductType { + &self.product_type + } + + pub fn product_id(&self) -> &str { + &self.product_id + } +} + +impl FromProto for ProductDeleteAction { + fn from_proto( + proto: protos::product_payload::ProductDeleteAction, + ) -> Result { + Ok(ProductDeleteAction { + product_type: ProductType::from_proto(proto.get_product_type())?, + product_id: proto.get_product_id().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_product_id(native.product_id().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 ProductDeleteAction +#[derive(Default, Clone)] +pub struct ProductDeleteActionBuilder { + product_type: Option, + product_id: 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_product_id(mut self, product_id: String) -> Self { + self.product_id = Some(product_id); + 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 product_id = self.product_id.ok_or_else(|| { + BuilderError::MissingField("'product_id' field is required".to_string()) + })?; + + Ok(ProductDeleteAction { + product_type, + product_id, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::protocol::schema::state::{DataType, PropertyValueBuilder}; + use std::fmt::Debug; + + #[test] + // Test that a product create action can be built correctly + fn test_product_create_builder() { + let action = ProductCreateActionBuilder::new() + .with_product_id("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_owner("Target".into()) + .with_properties(make_properties()) + .build() + .unwrap(); + + assert_eq!(action.product_id(), "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] + // Test that a product create action can be converted to bytes and back + fn test_product_create_into_bytes() { + let action = ProductCreateActionBuilder::new() + .with_product_id("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] + // Test that a product update action can be built correctly + fn test_product_update_builder() { + let action = ProductUpdateActionBuilder::new() + .with_product_id("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_properties(make_properties()) + .build() + .unwrap(); + + assert_eq!(action.product_id(), "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] + // Test that a product update action can be converted to bytes and back + fn test_product_update_into_bytes() { + let action = ProductUpdateActionBuilder::new() + .with_product_id("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .with_properties(make_properties()) + .build() + .unwrap(); + + test_from_bytes(action, ProductUpdateAction::from_bytes); + } + + #[test] + // Test that a product delete action can be built correctly + fn test_product_delete_builder() { + let action = ProductDeleteActionBuilder::new() + .with_product_id("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .build() + .unwrap(); + + assert_eq!(action.product_id(), "688955434684"); + assert_eq!(*action.product_type(), ProductType::GS1); + } + + #[test] + // Test that a product delete action can be converted to bytes and back + fn test_product_delete_into_bytes() { + let action = ProductDeleteActionBuilder::new() + .with_product_id("688955434684".into()) // GTIN-12 + .with_product_type(ProductType::GS1) + .build() + .unwrap(); + + test_from_bytes(action, ProductDeleteAction::from_bytes); + } + + #[test] + // Test that a product payload can be built correctly + fn test_product_payload_builder() { + let action = ProductCreateActionBuilder::new() + .with_product_id("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] + // Test that a product payload can be converted to bytes and back + fn test_product_payload_bytes() { + let action = ProductCreateActionBuilder::new() + .with_product_id("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 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 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..d8a2f98cc0 --- /dev/null +++ b/sdk/src/protocol/product/state.rs @@ -0,0 +1,576 @@ +// 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 { + product_id: String, + product_type: ProductType, + owner: String, + properties: Vec, +} + +impl Product { + pub fn product_id(&self) -> &str { + &self.product_id + } + + 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_product_id(self.product_id) + .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 { + product_id: product.get_product_id().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_product_id(product.product_id().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 product_id: Option, + pub product_type: Option, + pub owner: Option, + pub properties: Option>, +} + +impl ProductBuilder { + pub fn new() -> Self { + ProductBuilder::default() + } + + pub fn with_product_id(mut self, product_id: String) -> Self { + self.product_id = Some(product_id); + 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 product_id = self.product_id.ok_or_else(|| { + ProductBuildError::MissingField("'product_id' 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 { + product_id, + 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] + // Test that a product can be built correctly + fn test_product_builder() { + let product = build_product(); + + assert_eq!(product.product_id(), "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] + // Test that a product can be converted to a product builder + fn test_product_into_builder() { + let product = build_product(); + + let builder = product.into_builder(); + + assert_eq!(builder.product_id, 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] + // Test that a product can be converted to bytes and back + fn test_product_into_bytes() { + let builder = ProductBuilder::new(); + let original = builder + .with_product_id("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] + // Test that a product list can be built correctly + 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].product_id(), "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].product_id(), "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] + // Test that a product list can be converted to a product list builder + 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] + // Test that a product list can be converted to bytes and back + 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_product_id("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_product_id("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_product_id("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); + } +} From d61248670e492af66d1016e97e6e03652de322af Mon Sep 17 00:00:00 2001 From: Raphael Santo Domingo Date: Thu, 11 Jul 2019 16:50:20 -0500 Subject: [PATCH 2/2] Refactor product smart contract Other changes - Moved the 'check_permission' function call before checking for an agent's associated org - Renamed 'identifier field to 'product_id' field because 'identifier' is a rust error descriptor Signed-off-by: Raphael Santo Domingo --- contracts/product/src/handler.rs | 206 ++++++++++++++++--------------- contracts/product/src/payload.rs | 158 +++++++++--------------- contracts/product/src/state.rs | 111 ++++++++++------- 3 files changed, 229 insertions(+), 246 deletions(-) diff --git a/contracts/product/src/handler.rs b/contracts/product/src/handler.rs index aeff081543..f4daf18033 100644 --- a/contracts/product/src/handler.rs +++ b/contracts/product/src/handler.rs @@ -28,11 +28,15 @@ cfg_if! { } use grid_sdk::permissions::PermissionChecker; -use grid_sdk::protos::product_payload::*; -use grid_sdk::protos::product_state::Product; +use grid_sdk::protocol::product::payload::{ + Action, ProductCreateAction, ProductDeleteAction, ProductPayload, ProductUpdateAction, +}; +use grid_sdk::protocol::product::state::{ProductBuilder, ProductType}; + +use grid_sdk::protos::FromBytes; use crate::addressing::*; -use crate::payload::{Action, ProductPayload}; +use crate::payload::validate_payload; use crate::state::ProductState; use crate::validation::validate_gtin; @@ -76,11 +80,16 @@ impl ProductTransactionHandler { fn create_product( &self, - payload: ProductCreateAction, - mut state: ProductState, + payload: &ProductCreateAction, + state: &mut ProductState, signer: &str, perm_checker: &PermissionChecker, ) -> Result<(), ApplyError> { + let product_id = payload.product_id(); + let owner = payload.owner(); + let product_type = payload.product_type(); + let properties = payload.properties(); + // Check that the agent submitting the transactions exists in state let agent = match state.get_agent(signer)? { Some(agent) => agent, @@ -92,6 +101,9 @@ impl ProductTransactionHandler { } }; + // Check signing agent's permission + check_permission(perm_checker, signer, "can_create_product")?; + // Check that the agent has an organization associated with it if agent.org_id().is_empty() { return Err(ApplyError::InvalidTransaction(format!( @@ -100,49 +112,39 @@ impl ProductTransactionHandler { ))); } - // Check that the agent has the pike permission "can_create_product" for the organization - check_permission(perm_checker, signer, "can_create_product")?; + // Check if product exists in state + if state.get_product(product_id)?.is_some() { + return Err(ApplyError::InvalidTransaction(format!( + "Product already exists: {}", + product_id, + ))); + } // Check if the product type is a GS1 product - if payload.get_product_type() != ProductCreateAction_ProductType::GS1 { + if product_type != &ProductType::GS1 { return Err(ApplyError::InvalidTransaction( "Invalid product type enum for product".to_string(), )); } - // Use this varible to pass in the type correct enum (product_state) on product create - let product_type = grid_sdk::protos::product_state::Product_ProductType::GS1; - // Check if product identifier is a valid gtin - let product_id = payload.get_identifier(); + // Check if product product_id is a valid gtin if let Err(e) = validate_gtin(product_id) { return Err(ApplyError::InvalidTransaction(e.to_string())); } - // Check that the org owns the GS1 company prefix in the identifier - let org = match state.get_organization(payload.get_owner())? { + // Check that the org owns the GS1 company prefix in the product_id + let org = match state.get_organization(payload.owner())? { Some(org) => org, None => { return Err(ApplyError::InvalidTransaction(format!( - "The Agents organization does not exist: {}", - signer + "The Agent's organization does not exist: {}", + signer, ))); } }; - // Check if product exists in state - match state.get_product(product_id) { - Ok(Some(_)) => { - return Err(ApplyError::InvalidTransaction(format!( - "Product already exists: {}", - product_id - ))); - } - Ok(None) => (), - Err(err) => return Err(err), - } - /* Check if the agents organization contain GS1 Company Prefix key in its metadata - (gs1_company_prefixes), and the prefix must match the company prefix in the identifier */ + (gs1_company_prefixes), and the prefix must match the company prefix in the product_id */ let gs1_company_prefix_vec = org.metadata().to_vec(); let gs1_company_prefix_kv = match gs1_company_prefix_vec .iter() @@ -161,31 +163,38 @@ impl ProductTransactionHandler { // If the gtin identifer does not contain the organizations gs1 prefix if !product_id.contains(gs1_company_prefix_kv.value()) { return Err(ApplyError::InvalidTransaction(format!( - "The agents organization does not own the GS1 company prefix in the GTIN identifier: {:?}", + "The agents organization does not own the GS1 company prefix in the GTIN product_id: {:?}", org.metadata() ))); } } - let mut new_product = Product::new(); - new_product.set_identifier(product_id.to_string()); - new_product.set_owner(payload.get_owner().to_string()); - new_product.set_field_type(product_type); - new_product.set_product_values(protobuf::RepeatedField::from_vec( - payload.get_properties().to_vec(), - )); + let new_product = ProductBuilder::new() + .with_product_id(product_id.to_string()) + .with_owner(owner.to_string()) + .with_product_type(product_type.clone()) + .with_properties(properties.to_vec()) + .build() + .map_err(|err| { + ApplyError::InvalidTransaction(format!("Cannot build product: {}", err)) + })?; + + state.set_product(product_id, new_product)?; - state.set_product(signer, new_product)?; Ok(()) } fn update_product( &self, - payload: ProductUpdateAction, - mut state: ProductState, + payload: &ProductUpdateAction, + state: &mut ProductState, signer: &str, perm_checker: &PermissionChecker, ) -> Result<(), ApplyError> { + let product_id = payload.product_id(); + let product_type = payload.product_type(); + let properties = payload.properties(); + // Check that the agent submitting the transactions exists in state let agent = match state.get_agent(signer)? { Some(agent) => agent, @@ -197,22 +206,26 @@ impl ProductTransactionHandler { } }; + // Check signing agent's permission + check_permission(perm_checker, signer, "can_update_product")?; + + // Check that the agent has an organization associated with it + if agent.org_id().is_empty() { + return Err(ApplyError::InvalidTransaction(format!( + "The signing Agent does not have an associated organization: {}", + signer + ))); + } + // Check if the product type is a GS1 product - let product_type = payload.get_product_type(); - if product_type != ProductUpdateAction_ProductType::GS1 { + if product_type != &ProductType::GS1 { return Err(ApplyError::InvalidTransaction( "Invalid product type enum for product".to_string(), )); } - let product_id = payload.get_identifier(); - - // Check if product identifier is a valid gtin - if let Err(e) = validate_gtin(product_id) { - return Err(ApplyError::InvalidTransaction(e.to_string())); - } // Check if product exists - let mut product = match state.get_product(product_id) { + let product = match state.get_product(product_id) { Ok(Some(product)) => Ok(product), Ok(None) => Err(ApplyError::InvalidTransaction(format!( "No product exists: {}", @@ -222,31 +235,44 @@ impl ProductTransactionHandler { }?; // Check if the agent updating the product is part of the organization associated with the product - if product.get_owner() != agent.org_id() { + if product.owner() != agent.org_id() { return Err(ApplyError::InvalidTransaction( "Invalid organization for the agent submitting this transaction".to_string(), )); } - // Check that the agent has the pike permission "can_update_product" for the organization - check_permission(perm_checker, signer, "can_update_product")?; + // Check if product product_id is a valid gtin + if let Err(e) = validate_gtin(product_id) { + return Err(ApplyError::InvalidTransaction(e.to_string())); + } // Handle updating the product - let updated_product_values = payload.properties.clone(); - product.set_product_values(updated_product_values); + let updated_product = ProductBuilder::new() + .with_product_id(product_id.to_string()) + .with_owner(product.owner().to_string()) + .with_product_type(product_type.clone()) + .with_properties(properties.to_vec()) + .build() + .map_err(|err| { + ApplyError::InvalidTransaction(format!("Cannot build product: {}", err)) + })?; + + state.set_product(product_id, updated_product)?; - state.set_product(product_id, product)?; Ok(()) } fn delete_product( &self, - payload: ProductDeleteAction, - mut state: ProductState, + payload: &ProductDeleteAction, + state: &mut ProductState, signer: &str, perm_checker: &PermissionChecker, ) -> Result<(), ApplyError> { - // Check that the agent (signer) submitting the transactions exists in state + let product_id = payload.product_id(); + let product_type = payload.product_type(); + + // Check that the agent submitting the transactions exists in state let agent = match state.get_agent(signer)? { Some(agent) => agent, None => { @@ -257,19 +283,15 @@ impl ProductTransactionHandler { } }; + // Check signing agent's permission + check_permission(perm_checker, signer, "can_delete_product")?; + // Check if the product type is a GS1 product - let product_type = payload.get_product_type(); - if product_type != ProductDeleteAction_ProductType::GS1 { + if product_type != &ProductType::GS1 { return Err(ApplyError::InvalidTransaction( "Invalid product type enum for product".to_string(), )); } - let product_id = payload.get_identifier(); - - // Check if product identifier is a valid gtin - if let Err(e) = validate_gtin(product_id) { - return Err(ApplyError::InvalidTransaction(e.to_string())); - } // Check if product exists in state let product = match state.get_product(product_id) { @@ -281,16 +303,18 @@ impl ProductTransactionHandler { Err(err) => Err(err), }?; + // Check if product product_id is a valid gtin + if let Err(e) = validate_gtin(product_id) { + return Err(ApplyError::InvalidTransaction(e.to_string())); + } + // Check that the owner of the products organization is the same as the agent trying to delete the product - if product.get_owner() != agent.org_id() { + if product.owner() != agent.org_id() { return Err(ApplyError::InvalidTransaction( "Invalid organization for the agent submitting this transaction".to_string(), )); } - // Check that the agent deleting the product has the "can_delete_product" permission for the organization - check_permission(perm_checker, signer, "can_delete_product")?; - // Delete the product state.remove_product(product_id)?; Ok(()) @@ -315,45 +339,31 @@ impl TransactionHandler for ProductTransactionHandler { request: &TpProcessRequest, context: &mut dyn TransactionContext, ) -> Result<(), ApplyError> { - let payload = ProductPayload::new(request.get_payload()); - let payload = match payload { - Err(e) => return Err(e), - Ok(payload) => payload, - }; - let payload = match payload { - Some(x) => x, - None => { - return Err(ApplyError::InvalidTransaction(String::from( - "Request must contain a payload", - ))); - } - }; + let payload = ProductPayload::from_bytes(request.get_payload()).map_err(|err| { + ApplyError::InvalidTransaction(format!("Cannot build product payload: {}", err)) + })?; - if payload.get_timestamp().to_string().is_empty() { - return Err(ApplyError::InvalidTransaction(String::from( - "Timestamp is not set", - ))); - } + validate_payload(&payload)?; info!( "Grid Product Payload {:?} {}", - payload.get_action(), - payload.get_timestamp() + payload.action(), + payload.timestamp(), ); let signer = request.get_header().get_signer_public_key(); - let state = ProductState::new(context); + let mut state = ProductState::new(context); let perm_checker = PermissionChecker::new(context); - match payload.get_action() { - Action::CreateProduct(create_product_payload) => { - self.create_product(create_product_payload, state, signer, &perm_checker)? + match payload.action() { + Action::ProductCreate(create_product_payload) => { + self.create_product(create_product_payload, &mut state, signer, &perm_checker)? } - Action::UpdateProduct(update_product_payload) => { - self.update_product(update_product_payload, state, signer, &perm_checker)? + Action::ProductUpdate(update_product_payload) => { + self.update_product(update_product_payload, &mut state, signer, &perm_checker)? } - Action::DeleteProduct(delete_product_payload) => { - self.delete_product(delete_product_payload, state, signer, &perm_checker)? + Action::ProductDelete(delete_product_payload) => { + self.delete_product(delete_product_payload, &mut state, signer, &perm_checker)? } } Ok(()) diff --git a/contracts/product/src/payload.rs b/contracts/product/src/payload.rs index 2bf2fa5f5d..dc58e3301a 100644 --- a/contracts/product/src/payload.rs +++ b/contracts/product/src/payload.rs @@ -20,112 +20,70 @@ cfg_if! { } } -use grid_sdk::protos::product_payload::{ - ProductCreateAction, ProductCreateAction_ProductType, ProductDeleteAction, - ProductDeleteAction_ProductType, ProductPayload as Product_Payload_Proto, - ProductPayload_Action, ProductUpdateAction, ProductUpdateAction_ProductType, -}; +use grid_sdk::protocol::product::payload::{Action, ProductCreateAction, ProductPayload}; -#[derive(Debug, Clone)] -pub enum Action { - CreateProduct(ProductCreateAction), - UpdateProduct(ProductUpdateAction), - DeleteProduct(ProductDeleteAction), -} - -pub struct ProductPayload { - action: Action, - timestamp: u64, +pub fn validate_payload(payload: &ProductPayload) -> Result<(), ApplyError> { + validate_timestamp(*payload.timestamp())?; + match payload.action() { + Action::ProductCreate(action_payload) => validate_product_create_action(action_payload), + _ => Ok(()), + } } -impl ProductPayload { - pub fn new(payload: &[u8]) -> Result, ApplyError> { - let payload: Product_Payload_Proto = match protobuf::parse_from_bytes(payload) { - Ok(payload) => payload, - Err(_) => { - return Err(ApplyError::InvalidTransaction(String::from( - "Cannot deserialize payload", - ))); - } - }; - - let product_action = payload.get_action(); - let action = match product_action { - ProductPayload_Action::PRODUCT_CREATE => { - let product_create = payload.get_product_create(); - if product_create.get_identifier().is_empty() { - return Err(ApplyError::InvalidTransaction(String::from( - "Product id cannot be an empty string", - ))); - } - - if product_create.get_owner().is_empty() { - return Err(ApplyError::InvalidTransaction(String::from( - "Product owner cannot be an empty string", - ))); - } - - if product_create.get_product_type() == ProductCreateAction_ProductType::UNSET_TYPE - { - return Err(ApplyError::InvalidTransaction(String::from( - "Product type cannot be: UNSET_TYPE", - ))); - } - - Action::CreateProduct(product_create.clone()) - } - - ProductPayload_Action::PRODUCT_UPDATE => { - let product_update = payload.get_product_update(); - if product_update.get_identifier().is_empty() { - return Err(ApplyError::InvalidTransaction(String::from( - "Product id cannot be an empty string", - ))); - } - - if product_update.get_product_type() == ProductUpdateAction_ProductType::UNSET_TYPE - { - return Err(ApplyError::InvalidTransaction(String::from( - "Product type cannot be: UNSET_TYPE", - ))); - } - Action::UpdateProduct(product_update.clone()) - } - - ProductPayload_Action::PRODUCT_DELETE => { - let product_delete = payload.get_product_delete(); - if product_delete.get_identifier().is_empty() { - return Err(ApplyError::InvalidTransaction(String::from( - "Product id cannot be an empty string", - ))); - } - - if product_delete.get_product_type() == ProductDeleteAction_ProductType::UNSET_TYPE - { - return Err(ApplyError::InvalidTransaction(String::from( - "Product type cannot be: UNSET_TYPE", - ))); - } - - Action::DeleteProduct(product_delete.clone()) - } - - ProductPayload_Action::UNSET_ACTION => { - return Err(ApplyError::InvalidTransaction(String::from( - "No action specified", - ))); - } - }; - - let timestamp = payload.get_timestamp(); - Ok(Some(ProductPayload { action, timestamp })) +fn validate_product_create_action( + product_create_action: &ProductCreateAction, +) -> Result<(), ApplyError> { + if product_create_action.product_id() == "" { + return Err(ApplyError::InvalidTransaction(String::from( + "product_id cannot be empty string", + ))); + } + if product_create_action.owner() == "" { + return Err(ApplyError::InvalidTransaction(String::from( + "Owner cannot be empty string", + ))); } + Ok(()) +} - pub fn get_action(&self) -> Action { - self.action.clone() +fn validate_timestamp(timestamp: u64) -> Result<(), ApplyError> { + match timestamp { + 0 => Err(ApplyError::InvalidTransaction(String::from( + "Timestamp is not set", + ))), + _ => Ok(()), } +} - pub fn get_timestamp(&self) -> u64 { - self.timestamp +#[cfg(test)] +mod tests { + use super::*; + + use grid_sdk::protos::product_payload::{ + ProductCreateAction as ProductCreateActionProto, ProductPayload as ProductPayloadProto, + ProductPayload_Action as ActionProto, + }; + use grid_sdk::protos::product_state::Product_ProductType; + use grid_sdk::protos::IntoNative; + + #[test] + /// Test that an ok is returned if the payload with ProductCreateAction is valid. This test + /// needs to use the proto directly originally to be able to mimic the scenarios possbile + /// from creating a ProductCreateAction from bytes. This is because the + /// ProductCreateActionBuilder protects from building certain invalid payloads. + fn test_validate_payload_valid() { + let mut payload_proto = ProductPayloadProto::new(); + payload_proto.set_action(ActionProto::PRODUCT_CREATE); + payload_proto.set_timestamp(2); + let mut action = ProductCreateActionProto::new(); + action.set_product_id("my_product_id".to_string()); + action.set_owner("my_owner".to_string()); + action.set_product_type(Product_ProductType::GS1); + payload_proto.set_product_create(action); + let payload = payload_proto.clone().into_native().unwrap(); + assert!( + validate_payload(&payload).is_ok(), + "Payload should be valid" + ); } } diff --git a/contracts/product/src/state.rs b/contracts/product/src/state.rs index b4d3bd8460..d9c3b0a092 100644 --- a/contracts/product/src/state.rs +++ b/contracts/product/src/state.rs @@ -24,11 +24,8 @@ cfg_if! { use grid_sdk::protocol::pike::state::{Agent, AgentList}; use grid_sdk::protocol::pike::state::{Organization, OrganizationList}; -use grid_sdk::protos::product_state::Product; -use grid_sdk::protos::product_state::ProductList; -use grid_sdk::protos::FromBytes; - -use protobuf::Message; +use grid_sdk::protocol::product::state::{Product, ProductList, ProductListBuilder}; +use grid_sdk::protos::{FromBytes, IntoBytes}; use crate::addressing::*; @@ -41,12 +38,12 @@ impl<'a> ProductState<'a> { ProductState { context } } - pub fn get_product(&mut self, product_id: &str) -> Result, ApplyError> { + pub fn get_product(&self, product_id: &str) -> Result, ApplyError> { let address = make_product_address(product_id); //product id = gtin let d = self.context.get_state_entry(&address)?; match d { Some(packed) => { - let products: ProductList = match protobuf::parse_from_bytes(packed.as_slice()) { + let products = match ProductList::from_bytes(packed.as_slice()) { Ok(products) => products, Err(_) => { return Err(ApplyError::InternalError(String::from( @@ -55,45 +52,59 @@ impl<'a> ProductState<'a> { } }; + // find the product with the correct id Ok(products - .get_entries() + .products() .iter() - .find(|p| p.get_identifier() == product_id) + .find(|p| p.product_id() == product_id) .cloned()) } None => Ok(None), } } - pub fn set_product(&mut self, product_id: &str, product: Product) -> Result<(), ApplyError> { + pub fn set_product(&self, product_id: &str, product: Product) -> Result<(), ApplyError> { let address = make_product_address(product_id); let d = self.context.get_state_entry(&address)?; - let mut product_container = match d { - Some(packed) => match protobuf::parse_from_bytes(packed.as_slice()) { - Ok(products) => products, - Err(_) => { - return Err(ApplyError::InternalError(String::from( - "Cannot deserialize product list", + let mut products = match d { + Some(packed) => match ProductList::from_bytes(packed.as_slice()) { + Ok(product_list) => product_list.products().to_vec(), + Err(err) => { + return Err(ApplyError::InternalError(format!( + "Cannot deserialize product list: {:?}", + err ))); } }, - None => ProductList::new(), + None => vec![], }; - let mut products = product_container - .take_entries() - .into_iter() - .filter(|p| p.get_identifier() != product_id) - .collect::>(); - products.push(product); - products.sort_by(|p1, p2| p1.get_identifier().cmp(p2.get_identifier())); - product_container.set_entries(protobuf::RepeatedField::from_vec(products)); + let mut index = None; + for (i, product) in products.iter().enumerate() { + if product.product_id() == product_id { + index = Some(i); + break; + } + } - let serialized = match product_container.write_to_bytes() { + if let Some(i) = index { + products.remove(i); + } + products.push(product); + products.sort_by_key(|r| r.product_id().to_string()); + let product_list = ProductListBuilder::new() + .with_products(products) + .build() + .map_err(|err| { + ApplyError::InvalidTransaction(format!("Cannot build product list: {:?}", err)) + })?; + + let serialized = match product_list.into_bytes() { Ok(serialized) => serialized, - Err(_) => { - return Err(ApplyError::InternalError(String::from( - "Cannot serialize product list", + Err(err) => { + return Err(ApplyError::InvalidTransaction(format!( + "Cannot serialize product list: {:?}", + err ))); } }; @@ -104,32 +115,36 @@ impl<'a> ProductState<'a> { } // Currently product_id = gtin - pub fn remove_product(&mut self, product_id: &str) -> Result<(), ApplyError> { + pub fn remove_product(&self, product_id: &str) -> Result<(), ApplyError> { let address = make_product_address(product_id); let d = self.context.get_state_entry(&address)?; - let mut product_container = match d { - Some(packed) => match protobuf::parse_from_bytes(packed.as_slice()) { - Ok(products) => products, - Err(_) => { - return Err(ApplyError::InternalError(String::from( - "Cannot deserialize product list", + let products = match d { + Some(packed) => match ProductList::from_bytes(packed.as_slice()) { + Ok(product_list) => product_list.products().to_vec(), + Err(err) => { + return Err(ApplyError::InternalError(format!( + "Cannot deserialize product list: {:?}", + err ))); } }, - None => ProductList::new(), + None => vec![], }; // Collect a new vector of products without the removed item - let products = product_container - .take_entries() - .into_iter() - .filter(|p| p.get_identifier() != product_id) - .collect::>(); - - // Reset product list to the new vector - product_container.set_entries(protobuf::RepeatedField::from_vec(products)); - - let serialized = match product_container.write_to_bytes() { + let product_list = ProductListBuilder::new() + .with_products( + products + .into_iter() + .filter(|p| p.product_id() != product_id) + .collect::>(), + ) + .build() + .map_err(|err| { + ApplyError::InvalidTransaction(format!("Cannot build product list: {:?}", err)) + })?; + + let serialized = match product_list.into_bytes() { Ok(serialized) => serialized, Err(_) => { return Err(ApplyError::InternalError(String::from( @@ -171,7 +186,7 @@ impl<'a> ProductState<'a> { } } - pub fn get_organization(&mut self, id: &str) -> Result, ApplyError> { + pub fn get_organization(&self, id: &str) -> Result, ApplyError> { let address = compute_org_address(id); let d = self.context.get_state_entry(&address)?; match d {