diff --git a/Cargo.toml b/Cargo.toml index a5a35ed..36311fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ license = "Apache-2.0" name = "scaffolding-core" readme = "README.md" repository = "https://github.com/dsietz/scaffolding-core" -version = "0.1.0" +version = "0.2.0" [badges] maintenance = {status = "experimental"} @@ -26,7 +26,10 @@ path = "src/lib.rs" [dependencies] chrono = "0.4.35" -scaffolding-macros = {path = "./scaffolding-macros", version = "0.1.0"} +scaffolding-macros = {path = "./scaffolding-macros", version = "0.2.0"} +serde = "1.0.197" +serde_derive = "1.0" +serde_json = "1.0" [dependencies.uuid] features = ["v4"] diff --git a/README.md b/README.md index cebabed..3cca7e6 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,13 @@ For software development teams who appreciate a kick-start to their object orien --- ## What's New +| :warning: Please Note! | +| ----------------------------------------------------------------------------- | +| This crate is in an `beta` release phase and is only intended as experimental.| -!!! This crate is in an `Initial` release phase and is only intended as a PoC. !!! - -**0.1.0** -+ [Added Metadata feature](https://github.com/dsietz/scaffolding-core/issues/2) +**0.2.0** ++ [Add Activity Logging](https://github.com/dsietz/scaffolding-core/issues/18) ++ [Provide the ability to Serialize and Deserialize](https://github.com/dsietz/scaffolding-core/issues/19) ## How to Contribute diff --git a/scaffolding-macros/Cargo.toml b/scaffolding-macros/Cargo.toml index 8d9261e..b99a760 100644 --- a/scaffolding-macros/Cargo.toml +++ b/scaffolding-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scaffolding-macros" -version = "0.1.0" +version = "0.2.0" authors = ["dsietz "] edition = "2021" readme = "README.md" @@ -28,6 +28,9 @@ proc-macro = true [dependencies] quote = "1.0.35" syn = {version = "2.0.53", features = ["full", "extra-traits"]} +serde = "1.0.197" +serde_derive = "1.0" +serde_json = "1.0" [dev-dependencies] serde_yaml = "0.9.27" diff --git a/scaffolding-macros/src/lib.rs b/scaffolding-macros/src/lib.rs index 2dd3a8e..873f683 100644 --- a/scaffolding-macros/src/lib.rs +++ b/scaffolding-macros/src/lib.rs @@ -5,14 +5,16 @@ use syn::Expr::Struct; use syn::FieldValue; use syn::Member; use syn::{parse_macro_input, parse_quote, punctuated::Punctuated, ItemStruct, LitStr, Token}; +// use serde::Serialize; static METADATA: &str = "metadata"; -static CORE_ATTRS: [&str; 5] = [ +static CORE_ATTRS: [&str; 6] = [ "id", "created_dtm", "modified_dtm", "inactive_dtm", "expired_dtm", + "activity", ]; /// @@ -59,6 +61,13 @@ pub fn scaffolding_struct(args: TokenStream, input: TokenStream) -> TokenStream .unwrap(), ); + // The list of activity performed on the object + fields.named.push( + syn::Field::parse_named + .parse2(quote! { activity: Vec }) + .unwrap(), + ); + // optional attributes match attrs.contains(&METADATA.to_string()) { true => { @@ -110,9 +119,13 @@ fn impl_scaffolding(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; let gen = quote! { impl Scaffolding for #name { - // fn set_id(mut &self, value: String) { - // self.id = value; - // } + fn get_activity(&self, name: String) -> Vec{ + self.activity.iter().filter(|a| a.action == name).cloned().collect() + } + + fn log_activity(&mut self, name: String, descr: String) { + self.activity.push(ActivityItem::new(name, descr)); + } } }; gen.into() @@ -149,6 +162,7 @@ pub fn scaffolding_fn(args: TokenStream, input: TokenStream) -> TokenStream { "modified_dtm", "inactive_dtm", "expired_dtm", + "activity", ]; match attrs.contains(&METADATA.to_string()) { @@ -201,6 +215,10 @@ pub fn scaffolding_fn(args: TokenStream, input: TokenStream) -> TokenStream { let line: FieldValue = parse_quote! {expired_dtm: defaults::add_years(defaults::now(), 3)}; expr_struct.fields.insert(0, line); } + "activity" => { + let line: FieldValue = parse_quote! {activity: Vec::new()}; + expr_struct.fields.insert(0, line); + } "metadata" => { let line: FieldValue = parse_quote! {metadata: BTreeMap::new()}; diff --git a/scaffolding-macros/tests/macro_test.rs b/scaffolding-macros/tests/macro_test.rs.old similarity index 91% rename from scaffolding-macros/tests/macro_test.rs rename to scaffolding-macros/tests/macro_test.rs.old index 08a4745..381ddb0 100644 --- a/scaffolding-macros/tests/macro_test.rs +++ b/scaffolding-macros/tests/macro_test.rs.old @@ -1,11 +1,19 @@ // #[macro_use] extern crate scaffolding_macros; + #[cfg(test)] mod tests { use scaffolding_macros::*; use std::collections::BTreeMap; + #[derive(Debug, Clone)] + struct ActivityItem { + created_dtm: i64, + action: String, + description: String, + } + #[scaffolding_struct("metadata")] #[derive(Debug, Clone)] struct MyEntity { diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..89bb5a3 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,14 @@ +use std::error; +use std::fmt; + +// struct +#[derive(Debug, Clone)] +pub struct DeserializeError; + +//impl +impl fmt::Display for DeserializeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Unable to deserialize.") + } +} +impl error::Error for DeserializeError {} diff --git a/src/lib.rs b/src/lib.rs index 6c90ec3..86a058d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ //! //! ## Scaffolding Concept //! 1. A class that `extends` the "Scaffolding class" should inherate all the "parent" data structure and behavior, -//! as well as append the "child" specific data structure and behavior +//! as well as append the "child" specific data structure and behavior from the generic type being extended. //! 2. The developer should have the flexibility to adopt the default "parent" characteristics or overwrite them as desired. //! 3. There are common class attributes that are required in order to manage it using CRUD //! + `id` - The unique identifier of the object. @@ -17,6 +17,7 @@ //! + `modified_dtm` - The unix epoch (UTC) representation of when the object was last updated //! + `inactive_dtm` - The unix epoch (UTC) representation of when the object was/will be considered obsolete //! + `expired_dtm` - The unix epoch (UTC) representation of when the object was/will be ready for deletion +//! + `activity` - The list of actions performed on the object //! 4. There is common class behaviors that are required in order to manage it using CRUD //! + The `id` is not optional. It must be either provided or automatically generated during instantiation. //! This can be done by calling the `Scaffolding` trait's `id()` method @@ -27,20 +28,22 @@ //! + The `inactive_dtm` is not optional. It must be either provided or automatically generated during instantiation or updates to the object. //! This can be done by calling one of the `Scaffolding` trait's many datetime related methods, (e.g.: `add_months()` in conjuctions with `now()`) //! + The `expire_dtm` is not optional. It must be either provided or automatically generated during instantiation or updates to the object. -//! This can be done by calling one of the `Scaffolding` trait's many datetime related methods, (e.g.: `never()`) +//! This can be done by calling one of the `Scaffolding` trait's many datetime related methods, (e.g.: `never()`) +//! + The `activity` is required and by default is an empty list of activity //! //! ### Example //! Add Scaffolding to a `struct` and `impl` using macros and defaults //! ```rust //! extern crate scaffolding_core; //! -//! use scaffolding_core::{defaults}; +//! use scaffolding_core::{defaults, ActivityItem, Scaffolding}; //! use scaffolding_macros::*; +//! use serde_derive::{Deserialize, Serialize}; //! // Required for scaffolding metadata functionality //! use std::collections::BTreeMap; //! //! #[scaffolding_struct("metadata")] -//! #[derive(Debug, Clone)] +//! #[derive(Debug, Clone, Deserialize, Serialize, Scaffolding)] //! struct MyEntity { //! a: bool, //! b: String, @@ -72,6 +75,11 @@ //! assert_eq!(entity.inactive_dtm, defaults::add_days(defaults::now(), 90)); //! // expires in 3 years //! assert_eq!(entity.expired_dtm, defaults::add_years(defaults::now(), 3)); +//! +//! // add activity to the activty log +//! entity.log_activity("cancelled".to_string(), "The customer has cancelled their service".to_string()); +//! assert_eq!(entity.get_activity("cancelled".to_string()).len(), 1); +//! //! // use the metadata functionality //! entity.metadata.insert("field_1".to_string(), "myvalue".to_string()); //! assert_eq!(entity.metadata.len(), 1); @@ -83,8 +91,239 @@ //! // extended behavior //! assert_eq!(entity.my_func(), "my function"); //! ``` +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + +use errors::*; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// Supporting Classes +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ActivityItem { + pub created_dtm: i64, + pub action: String, + pub description: String, +} + +impl ActivityItem { + pub fn new(name: String, descr: String) -> Self { + Self { + created_dtm: defaults::now(), + action: name, + description: descr, + } + } + + pub fn deserialized(serialized: &[u8]) -> Result { + match serde_json::from_slice(&serialized) { + Ok(item) => Ok(item), + Err(err) => { + println!("{}", err); + Err(DeserializeError) + } + } + } + + pub fn serialize(&mut self) -> String { + serde_json::to_string(&self).unwrap() + } +} /// The core behavior of an Scaffolding object -pub trait Scaffolding {} +pub trait Scaffolding { + /// This function adds a ActivityItem to the activity log + /// + /// #Example + /// + /// ```rust + /// #[macro_use] + /// // extern crate scaffolding_core; + /// // extern crate scaffolding_macros; + /// + /// use scaffolding_core::{defaults, ActivityItem, Scaffolding}; + /// use scaffolding_macros::*; + /// use serde_derive::{Deserialize, Serialize}; + /// + /// #[scaffolding_struct] + /// #[derive(Clone, Debug, Deserialize, Serialize, Scaffolding)] + /// struct MyEntity {} + /// + /// impl MyEntity { + /// #[scaffolding_fn] + /// fn new() -> Self { + /// Self {} + /// } + /// } + /// + /// let mut entity = MyEntity::new(); + /// + /// entity.log_activity("cancelled".to_string(), "The customer has cancelled their service".to_string()); + /// assert_eq!(entity.activity.len(), 1); + /// ``` + fn log_activity(&mut self, name: String, descr: String); + /// This function retrieves all the ActivityItems that have the specified action (name) + /// + /// #Example + /// + /// ```rust + /// #[macro_use] + /// // extern crate scaffolding_core; + /// // extern crate scaffolding_macros; + /// + /// use scaffolding_core::{defaults, ActivityItem, Scaffolding}; + /// use scaffolding_macros::*; + /// use serde_derive::{Deserialize, Serialize}; + /// + /// #[scaffolding_struct] + /// #[derive(Clone, Debug, Deserialize, Serialize, Scaffolding)] + /// struct MyEntity {} + /// + /// impl MyEntity { + /// #[scaffolding_fn] + /// fn new() -> Self { + /// Self {} + /// } + /// } + /// + /// let mut entity = MyEntity::new(); + /// + /// entity.log_activity("ordered".to_string(), "The customer has place the order".to_string()); + /// entity.log_activity("cancelled".to_string(), "The customer has cancelled their service".to_string()); + /// assert_eq!(entity.get_activity("cancelled".to_string()).len(), 1); + /// ``` + fn get_activity(&self, name: String) -> Vec; + /// This function instantiates an entity from a JSON string. + /// + /// #Example + /// + /// ```rust + /// #[macro_use] + /// // extern crate scaffolding_core; + /// // extern crate scaffolding_macros; + /// + /// use scaffolding_core::{defaults, ActivityItem, Scaffolding}; + /// use scaffolding_macros::*; + /// use serde_derive::{Deserialize, Serialize}; + /// + /// #[scaffolding_struct] + /// #[derive(Clone, Debug, Deserialize, Serialize, Scaffolding)] + /// struct MyEntity {} + /// + /// impl MyEntity { + /// #[scaffolding_fn] + /// fn new() -> Self { + /// Self {} + /// } + /// } + /// + /// let json = r#"{ + /// "id":"b4d6c6db-7468-400a-8536-a5e83b1f2bdc", + /// "created_dtm":1711802687, + /// "modified_dtm":1711802687, + /// "inactive_dtm":1719578687, + /// "expired_dtm":1806410687, + /// "activity":[ + /// { + /// "created_dtm":1711802687, + /// "action":"updated", + /// "description":"The object has been updated" + /// }, + /// { + /// "created_dtm":1711802687, + /// "action":"updated", + /// "description":"The object has been updated" + /// }, + /// { + /// "created_dtm":1711802687, + /// "action":"cancelled", + /// "description":"The object has been cancelled" + /// } + /// ] + /// }"#; + /// let deserialized = MyEntity::deserialized::(json.as_bytes()).unwrap(); + /// + /// assert_eq!(deserialized.id, "b4d6c6db-7468-400a-8536-a5e83b1f2bdc"); + /// assert_eq!(deserialized.activity.len(), 3); + /// + /// ``` + fn deserialized(serialized: &[u8]) -> Result { + match serde_json::from_slice::(&serialized) { + Ok(item) => Ok(item), + Err(err) => { + println!("{}", err); + Err(DeserializeError) + } + } + } + /// This function converts the entity to a serialize JSON string. + /// + /// #Example + /// + /// ```rust + /// #[macro_use] + /// // extern crate scaffolding_core; + /// // extern crate scaffolding_macros; + /// + /// use scaffolding_core::{defaults, ActivityItem, Scaffolding}; + /// use scaffolding_macros::*; + /// use serde_derive::{Deserialize, Serialize}; + /// + /// #[scaffolding_struct] + /// #[derive(Clone, Debug, Deserialize, Serialize, Scaffolding)] + /// struct MyEntity {} + /// + /// impl MyEntity { + /// #[scaffolding_fn] + /// fn new() -> Self { + /// Self {} + /// } + /// } + /// + /// let mut entity = MyEntity::new(); + /// let json_string = entity.serialize(); + /// + /// println!("{}", json_string); + /// ``` + fn serialize(&mut self) -> String + where + Self: Serialize, + { + serde_json::to_string(&self).unwrap() + } +} pub mod defaults; +pub mod errors; + +#[cfg(test)] +mod tests { + use crate::{defaults, ActivityItem}; + + fn get_actionitem() -> ActivityItem { + ActivityItem::new( + "updated".to_string(), + "The object has been updated.".to_string(), + ) + } + #[test] + fn test_activityitem_new() { + let ai = get_actionitem(); + + assert_eq!(ai.created_dtm, defaults::now()); + assert_eq!(ai.action, "updated".to_string()); + assert_eq!(ai.description, "The object has been updated.".to_string()); + } + + #[test] + fn test_activityitem_serialization() { + let serialized = r#"{"created_dtm":1711760135,"action":"updated","description":"The object has been updated."}"#; + let mut ai = ActivityItem::deserialized(&serialized.as_bytes()).unwrap(); + + assert_eq!(ai.created_dtm, 1711760135); + assert_eq!(ai.action, "updated".to_string()); + assert_eq!(ai.description, "The object has been updated.".to_string()); + assert_eq!(ai.serialize(), serialized); + } +} diff --git a/tests/core_test.rs b/tests/core_test.rs index 5e186c5..710152f 100644 --- a/tests/core_test.rs +++ b/tests/core_test.rs @@ -1,15 +1,15 @@ -#[macro_use] extern crate scaffolding_core; extern crate scaffolding_macros; #[cfg(test)] mod tests { use chrono::Utc; - use scaffolding_core::{defaults, Scaffolding}; + use scaffolding_core::{defaults, ActivityItem, Scaffolding}; use scaffolding_macros::*; + use serde_derive::{Deserialize, Serialize}; #[scaffolding_struct] - #[derive(Debug, Clone, Scaffolding)] + #[derive(Clone, Debug, Deserialize, Serialize, Scaffolding)] struct MyEntity { b: bool, n: i64, @@ -52,4 +52,108 @@ mod tests { // extended behavior assert_eq!(entity.my_func(), "my function"); } + + #[test] + fn test_entity_activity() { + let mut entity = MyEntity::new(true); + + entity.log_activity( + "updated".to_string(), + "The object has been updated".to_string(), + ); + entity.log_activity( + "updated".to_string(), + "The object has been updated".to_string(), + ); + entity.log_activity( + "cancelled".to_string(), + "The object has been cancelled".to_string(), + ); + + assert_eq!(entity.activity.len(), 3); + assert_eq!(entity.get_activity("updated".to_string()).len(), 2); + } + + #[test] + fn test_entity_deserialize() { + let never = 253402261199; + let json = r#"{ + "b":true, + "n":253402261199, + "id":"b4d6c6db-7468-400a-8536-a5e83b1f2bdc", + "created_dtm":1711802687, + "modified_dtm":1711802687, + "inactive_dtm":1719578687, + "expired_dtm":1806410687, + "activity":[ + { + "created_dtm":1711802687, + "action":"updated", + "description":"The object has been updated" + }, + { + "created_dtm":1711802687, + "action":"updated", + "description":"The object has been updated" + }, + { + "created_dtm":1711802687, + "action":"cancelled", + "description":"The object has been cancelled" + } + ] + }"#; + let deserialized = MyEntity::deserialized::(json.as_bytes()).unwrap(); + assert_eq!(deserialized.id, "b4d6c6db-7468-400a-8536-a5e83b1f2bdc"); + assert_eq!(deserialized.b, true); + assert_eq!(deserialized.n, never); + assert_eq!(deserialized.my_func(), "my function"); + } + + #[test] + #[ignore] + fn test_entity_serialize() { + let mut entity = MyEntity::new(true); + entity.log_activity( + "updated".to_string(), + "The object has been updated".to_string(), + ); + entity.log_activity( + "updated".to_string(), + "The object has been updated".to_string(), + ); + entity.log_activity( + "cancelled".to_string(), + "The object has been cancelled".to_string(), + ); + + let expected = r#"{ + "b":true, + "n":253402261199, + "id":"b4d6c6db-7468-400a-8536-a5e83b1f2bdc", + "created_dtm":1711802687, + "modified_dtm":1711802687, + "inactive_dtm":1719578687, + "expired_dtm":1806410687, + "activity":[ + { + "created_dtm":1711802687, + "action":"updated", + "description":"The object has been updated" + }, + { + "created_dtm":1711802687, + "action":"updated", + "description":"The object has been updated" + }, + { + "created_dtm":1711802687, + "action":"cancelled", + "description":"The object has been cancelled" + } + ] + }"#; + + assert_eq!(entity.serialize(), expected); + } } diff --git a/tests/metadata_test.rs b/tests/metadata_test.rs index 933e927..d9b4dd3 100644 --- a/tests/metadata_test.rs +++ b/tests/metadata_test.rs @@ -1,10 +1,9 @@ -#[macro_use] extern crate scaffolding_core; extern crate scaffolding_macros; #[cfg(test)] mod tests { - use scaffolding_core::defaults; + use scaffolding_core::{defaults, ActivityItem}; use scaffolding_macros::*; use std::collections::BTreeMap;