use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Atom {
uuid: String,
source_schema_name: String,
source_pub_key: String,
source_file_name: Option<String>,
created_at: DateTime<Utc>,
prev_atom_uuid: Option<String>,
content: Value,
status: AtomStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AtomStatus {
Active,
Deleted,
}
impl Atom {
fn generate_content_uuid(source_schema_name: &str, content: &Value) -> String {
let mut hasher = Sha256::new();
hasher.update(source_schema_name.as_bytes());
hasher.update(content.to_string().as_bytes());
let hash = hasher.finalize();
format!("{:x}", hash)
}
#[must_use]
pub fn new(source_schema_name: String, source_pub_key: String, content: Value) -> Self {
let uuid = Self::generate_content_uuid(&source_schema_name, &content);
Self {
uuid,
source_schema_name,
source_pub_key,
source_file_name: None,
created_at: Utc::now(),
prev_atom_uuid: None,
content,
status: AtomStatus::Active,
}
}
#[must_use]
pub fn with_prev_version(mut self, prev_atom_uuid: String) -> Self {
self.prev_atom_uuid = Some(prev_atom_uuid);
self
}
#[must_use]
pub fn with_source_file_name(mut self, file_name: String) -> Self {
self.source_file_name = Some(file_name);
self
}
pub fn with_status(mut self, status: AtomStatus) -> Self {
self.status = status;
self
}
#[must_use]
pub const fn content(&self) -> &Value {
&self.content
}
pub fn set_status(&mut self, status: AtomStatus) {
self.status = status;
}
#[must_use]
pub fn get_transformed_content(&self, transform: &str) -> Value {
match transform {
"lowercase" => {
if let Value::String(s) = &self.content {
Value::String(s.to_lowercase())
} else {
self.content.clone()
}
}
_ => self.content.clone(),
}
}
#[must_use]
pub fn uuid(&self) -> &str {
&self.uuid
}
#[must_use]
pub fn source_schema_name(&self) -> &str {
&self.source_schema_name
}
#[must_use]
pub fn source_pub_key(&self) -> &str {
&self.source_pub_key
}
#[must_use]
pub fn source_file_name(&self) -> Option<&String> {
self.source_file_name.as_ref()
}
#[must_use]
pub const fn created_at(&self) -> DateTime<Utc> {
self.created_at
}
#[must_use]
pub const fn prev_atom_uuid(&self) -> Option<&String> {
self.prev_atom_uuid.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_atom_creation() {
let content = json!({
"name": "test",
"value": 42
});
let atom = Atom::new(
"test_schema".to_string(),
"test_key".to_string(),
content.clone(),
);
assert_eq!(atom.source_schema_name(), "test_schema");
assert_eq!(atom.source_pub_key(), "test_key");
assert_eq!(atom.prev_atom_uuid(), None);
assert_eq!(atom.content(), &content);
assert!(!atom.uuid().is_empty());
assert!(atom.created_at() <= Utc::now());
}
#[test]
fn test_atom_with_prev_reference() {
let first_atom = Atom::new(
"test_schema".to_string(),
"test_key".to_string(),
json!({"version": 1}),
);
let second_atom =
Atom::with_prev_version(first_atom.clone(), first_atom.uuid().to_string());
assert_eq!(
second_atom.prev_atom_uuid(),
Some(&first_atom.uuid().to_string())
);
}
#[test]
fn test_molecule_creation_and_update() {
use crate::atom::Molecule;
use crate::atom::MoleculeBehavior;
let atom = Atom::new(
"test_schema".to_string(),
"test_key".to_string(),
json!({"test": true}),
);
let molecule = Molecule::new(atom.uuid().to_string(), "test_key".to_string());
assert_eq!(molecule.get_atom_uuid(), &atom.uuid().to_string());
let new_atom = Atom::new(
"test_schema".to_string(),
"test_key".to_string(),
json!({"test": false}),
)
.with_prev_version(atom.uuid().to_string());
let mut updated_ref = molecule.clone();
updated_ref.set_atom_uuid(new_atom.uuid().to_string());
assert_eq!(updated_ref.get_atom_uuid(), &new_atom.uuid().to_string());
assert!(updated_ref.updated_at() >= molecule.updated_at());
}
}