use chrono::{DateTime, FixedOffset};
use omry_archiving::record::LanguageRecord;
use omry_archiving::{
ClientDateTimeRecord, Document, Marshal, RawRecord, Record, RecordMeta, RecordParams, ToRecord,
};
use thiserror::Error;
#[derive(Debug, Error)]
#[error(transparent)]
pub struct DbRecordError(#[from] DbRecordErrorRepr);
#[derive(Debug, Error)]
enum DbRecordErrorRepr {
#[error("Error (de)serializing the web page: {0}")]
WebArchive(String),
#[error("This record is missing an ID.")]
MissingId,
}
#[derive(Debug)]
pub struct DbRecord {
pub(crate) id: Option<i64>,
pub(crate) url: String,
pub(crate) client_datetime: DateTime<FixedOffset>,
pub(crate) client_datetime_raw: Option<String>,
pub(crate) timestamp_flora: i64,
pub(crate) title: String,
pub(crate) language: Option<String>,
pub(crate) document: Vec<u8>,
pub(crate) updated_at: Option<i64>,
}
impl DbRecord {
#[must_use]
pub fn id(&self) -> Option<i64> {
self.id
}
}
impl TryFrom<Record> for DbRecord {
type Error = DbRecordError;
fn try_from(value: Record) -> Result<Self, Self::Error> {
let id = value.id();
let url = value.url().to_string();
let title = value.title().to_string();
let (client_datetime, client_datetime_raw) = match value.client_datetime() {
ClientDateTimeRecord::Parsed(datetime) => (*datetime, None),
ClientDateTimeRecord::Interpolated { value, raw } => (*value, Some(raw.clone())),
};
let timestamp_flora = value.timestamp_archived();
let language = value.language().map(|lang| {
lang.primary()
.unwrap_or_else(|error| error.raw_data().to_string())
});
let web_archive_serialized = value.document().to_byte_vec().map_err(|error| {
DbRecordErrorRepr::WebArchive(format!("Couldn't serialize the archive: {error}"))
})?;
Ok(Self {
id,
url,
client_datetime,
client_datetime_raw,
timestamp_flora,
title,
language,
document: web_archive_serialized,
updated_at: value.updated_at(),
})
}
}
impl ToRecord for DbRecord {
type Error = DbRecordError;
fn to_record(&self) -> Result<Record, Self::Error> {
let document = Document::from_bytes(&self.document)
.map_err(|error| DbRecordErrorRepr::WebArchive(error.to_string()))?;
let params = RecordParams {
id: self.id,
url: self.url.clone(),
title: self.title.clone(),
client_datetime: self.client_datetime.to_rfc3339(),
timestamp_flora: Some(self.timestamp_flora),
language: self.language.clone(),
document,
updated_at: self.updated_at,
};
Ok(Record::new(params))
}
}
impl PartialEq for DbRecord {
fn eq(&self, other: &Self) -> bool {
let ids_considered_equal = if let (Some(this_id), Some(other_id)) = (self.id, other.id) {
this_id == other_id
} else {
true
};
self.url == other.url
&& self.client_datetime == other.client_datetime
&& self.client_datetime_raw == other.client_datetime_raw
&& self.timestamp_flora == other.timestamp_flora
&& self.title == other.title
&& self.language == other.language
&& self.document == other.document
&& ids_considered_equal
&& self.updated_at == other.updated_at
}
}
impl TryFrom<DbRecord> for RawRecord {
type Error = DbRecordError;
fn try_from(rec: DbRecord) -> Result<Self, Self::Error> {
let DbRecord {
id,
url,
client_datetime,
client_datetime_raw,
timestamp_flora,
title,
language,
document,
updated_at,
} = rec;
let id = id.ok_or(DbRecordErrorRepr::MissingId)?;
Ok(Self {
id,
url,
client_datetime,
client_datetime_raw,
timestamp_flora,
title,
language,
document,
updated_at,
})
}
}
#[derive(Debug, PartialEq)]
pub struct DbRecordMeta {
pub(crate) id: i64,
pub(crate) url: String,
pub(crate) client_datetime: DateTime<FixedOffset>,
pub(crate) timestamp_flora: i64,
pub(crate) title: String,
pub(crate) language: Option<String>,
pub(crate) updated_at: Option<i64>,
}
impl From<DbRecordMeta> for RecordMeta {
fn from(record: DbRecordMeta) -> Self {
let DbRecordMeta {
id,
url,
client_datetime,
timestamp_flora,
title,
language,
updated_at,
} = record;
let client_datetime = ClientDateTimeRecord::new(&client_datetime.to_rfc3339());
RecordMeta {
id,
url,
title,
client_datetime,
timestamp_flora,
language: language.map(LanguageRecord::new),
updated_at,
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use anyhow::{Context, Result};
use omry_archiving::{Document, Marshal, record::RecordParams};
use pretty_assertions::assert_eq;
use std::env;
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::PathBuf;
use super::*;
const TEST_ARCHIVE_PATH: &str = "test_data/example.postcard";
const WORKSPACE_DIR_KEY: &str = "CARGO_WORKSPACE_DIR";
fn get_test_path(path: &str) -> Result<PathBuf> {
let workspace_dir_path = env::var(WORKSPACE_DIR_KEY)
.context("WORKSPACE_DIR_KEY is not set")
.map(fs::canonicalize)??;
Ok(workspace_dir_path.join(path))
}
fn load_test_archive_holder() -> Result<Document> {
let path = get_test_path(TEST_ARCHIVE_PATH)?;
let mut file = File::open(&path)?;
let mut buf = vec![];
file.read_to_end(&mut buf)?;
let archive_holder = Document::from_bytes(&buf)?;
Ok(archive_holder)
}
pub(crate) fn test_record_params() -> Result<RecordParams> {
let document = load_test_archive_holder()?;
let url = "https://example.org/".to_string();
let title = "Example Domain";
let client_datetime = "2024-10-11T13:49:46-05:00";
#[allow(clippy::unreadable_literal)]
let timestamp_flora = Some(1728695243i64);
let language = Some("en".to_string());
Ok(RecordParams {
id: None,
url,
title: title.to_string(),
client_datetime: client_datetime.to_string(),
timestamp_flora,
language,
document,
updated_at: None,
})
}
#[allow(clippy::expect_used)]
#[test]
fn can_convert_record_to_db_record() -> Result<()> {
let record = Record::new(test_record_params()?);
let same_record = Record::new(test_record_params()?);
let primary_language_tag = same_record.language().map(|lang| {
lang.primary()
.expect("Couldn't compute the primary language tag")
});
let db_record: DbRecord = record.try_into()?;
assert_eq!(db_record.title, same_record.title());
assert_eq!(db_record.language, primary_language_tag);
Ok(())
}
#[test]
fn same_record_is_equal() -> anyhow::Result<()> {
let record: DbRecord = Record::new(test_record_params()?).try_into()?;
assert_eq!(record, record);
Ok(())
}
#[test]
fn different_records_are_not_equal() -> Result<()> {
let record = Record::new(test_record_params()?);
let mut other_record_params = test_record_params()?;
other_record_params.url = "https://example.com".into();
let other_record = Record::new(other_record_params);
let record: DbRecord = record.try_into()?;
let other_record = other_record.try_into()?;
assert_ne!(record, other_record);
Ok(())
}
#[test]
fn record_can_roundtrip_through_db_record() -> Result<()> {
let record = Record::new(test_record_params()?);
let db_record: DbRecord = record.try_into()?;
let roundtripped_record = db_record.to_record()?;
let same_record = Record::new(test_record_params()?);
assert_eq!(same_record, roundtripped_record);
Ok(())
}
}