use crate::bridge::library::{client as library_client, mirror::options::LibraryTransport};
use indicatrix_net::library::{DesignRecord, DesignSummary, LibraryRequest, LibraryResponse};
use indicatrix_vault::db::sqlite::Database;
use std::{
cell::RefCell,
collections::HashMap,
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
};
pub(super) struct FakeTransport {
search_result: Vec<DesignSummary>,
page_size: usize,
designs: HashMap<i64, DesignRecord>,
attachments: HashMap<i64, (String, Vec<u8>)>,
pub(super) search_calls: AtomicUsize,
pub(super) fetch_design_calls: AtomicUsize,
pub(super) fetch_attachment_calls: AtomicUsize,
pub(super) fetched_entry_ids: RefCell<Vec<i64>>,
fail_fetch_design_for: Option<i64>,
}
impl FakeTransport {
pub(super) fn new(summaries: Vec<DesignSummary>, designs: Vec<DesignRecord>) -> Self {
Self {
search_result: summaries,
page_size: usize::MAX,
designs: designs.into_iter().map(|d| (d.entry_id, d)).collect(),
attachments: HashMap::new(),
search_calls: AtomicUsize::new(0),
fetch_design_calls: AtomicUsize::new(0),
fetch_attachment_calls: AtomicUsize::new(0),
fetched_entry_ids: RefCell::new(Vec::new()),
fail_fetch_design_for: None,
}
}
pub(super) fn with_attachment(mut self, id: i64, name: &str, content: Vec<u8>) -> Self {
self.attachments.insert(id, (name.to_string(), content));
self
}
pub(super) fn with_page_size(mut self, page_size: usize) -> Self {
self.page_size = page_size;
self
}
pub(super) fn with_fetch_design_failure(mut self, entry_id: i64) -> Self {
self.fail_fetch_design_for = Some(entry_id);
self
}
}
impl LibraryTransport for FakeTransport {
fn request(
&self,
req: &LibraryRequest,
) -> Result<LibraryResponse, library_client::LibraryClientError> {
match req {
LibraryRequest::Search { .. } => {
unreachable!(
"mirror sync always pages via SearchPage now, never sends Search directly"
)
}
LibraryRequest::SearchPage { cursor, .. } => {
self.search_calls.fetch_add(1, Ordering::Relaxed);
let start = cursor.map_or(0, |after| {
self.search_result
.iter()
.position(|s| s.entry_id == after)
.map_or(self.search_result.len(), |i| i + 1)
});
let end = start
.saturating_add(self.page_size)
.min(self.search_result.len());
let page = self.search_result[start..end].to_vec();
let next_cursor = if page.len() == self.page_size {
page.last().map(|s| s.entry_id)
} else {
None
};
Ok(LibraryResponse::SearchResultsPage {
results: page,
next_cursor,
excluded_for_missing_curves: 0,
})
}
LibraryRequest::FetchDesign { entry_id } => {
self.fetch_design_calls.fetch_add(1, Ordering::Relaxed);
self.fetched_entry_ids.borrow_mut().push(*entry_id);
if self.fail_fetch_design_for == Some(*entry_id) {
return Err(library_client::LibraryClientError::Client(
indicatrix_net::client::ClientError::Net(
indicatrix_net::messages::NetError::Framing(
indicatrix_net::framing::FramingError::Io(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"connection dropped and could not be re-established",
)),
),
),
));
}
Ok(self
.designs
.get(entry_id)
.map_or(LibraryResponse::NotFound, |d| {
LibraryResponse::Design(Box::new(d.clone()))
}))
}
LibraryRequest::FetchAttachment { attachment_id } => {
self.fetch_attachment_calls.fetch_add(1, Ordering::Relaxed);
match self.attachments.get(attachment_id) {
Some((name, content)) => Ok(LibraryResponse::Attachment {
name: name.clone(),
content: content.clone(),
}),
None => Ok(LibraryResponse::NotFound),
}
}
LibraryRequest::FilterOptions => unreachable!("mirror sync never sends this"),
LibraryRequest::FetchDesignSource { .. } => {
unreachable!(
"mirror sync never sends this -- only the editor's remote 'Load Selected' does"
)
}
}
}
}
pub(super) fn temp_db() -> (Arc<Mutex<Database>>, std::path::PathBuf) {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"indicatrix-cut-library-mirror-test-{}-{n}.sqlite",
std::process::id()
));
let _ = std::fs::remove_file(&path);
let db = Database::new(Some(path.to_str().unwrap())).unwrap();
(Arc::new(Mutex::new(db)), path)
}
pub(super) fn design_record(
entry_id: i64,
title: &str,
url: &str,
extra_field: &str,
) -> DesignRecord {
let mut record = DesignRecord {
entry_id,
title: title.to_string(),
url: url.to_string(),
design_id: Some(format!("D-{entry_id}")),
page_url: url.to_string(),
diagram_image_name: None,
diagram_image_data: None,
competition_diagram: None,
lw_ratio: Some(extra_field.to_string()),
refractive_index: Some("1.72".to_string()),
index_gear: Some("96".to_string()),
volume: Some("0.65".to_string()),
facets_count: Some("57".to_string()),
shape: Some("Round".to_string()),
designer_info: Some("Capps, Jerry".to_string()),
preview_material: None,
hw_ratio: None,
tw_ratio: None,
uw_ratio: None,
pw_ratio: None,
cw_ratio: None,
symmetry_order: None,
mirror_symmetry: None,
designer: None,
angle_settings: vec![indicatrix_net::library::AngleSettingWire {
order_index: 0,
facet: "P1".to_string(),
angle: "41.0".to_string(),
index: "96".to_string(),
notes: String::new(),
}],
attachments: Vec::new(),
version: [0u8; 32],
};
record.version = content_hash(&[title.as_bytes(), url.as_bytes(), extra_field.as_bytes()]);
record
}
pub(super) fn design_summary(
entry_id: i64,
title: &str,
url: &str,
extra_field: &str,
) -> DesignSummary {
DesignSummary {
entry_id,
title: title.to_string(),
url: url.to_string(),
design_id: Some(format!("D-{entry_id}")),
shape: Some("Round".to_string()),
index_gear: Some("96".to_string()),
facets_count: Some("57".to_string()),
designer_info: Some("Capps, Jerry".to_string()),
lw_ratio: Some(extra_field.to_string()),
refractive_index: Some("1.72".to_string()),
volume: Some("0.65".to_string()),
competition_diagram: None,
ignored: false,
version: content_hash(&[title.as_bytes(), url.as_bytes(), extra_field.as_bytes()]),
}
}
fn content_hash(parts: &[&[u8]]) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
for p in parts {
hasher.update(p);
}
hasher.finalize().into()
}