use std::collections::HashMap;
pub trait CurrentLayer {
fn export(&self, tile_ids: &[&str]) -> Vec<u8>;
fn import(&mut self, data: &[u8]) -> ImportResult;
fn transport_capacity(&self) -> usize;
}
#[derive(Debug, Clone)]
pub struct TransportTile {
pub id: String,
pub content: String,
pub source: String,
pub weight: f32,
pub tier: DeployTier,
pub created_at: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeployTier {
Live = 0,
Monitored = 1,
HumanGated = 2,
}
impl DeployTier {
pub fn from_byte(b: u8) -> Self {
match b {
0 => DeployTier::Live,
1 => DeployTier::Monitored,
_ => DeployTier::HumanGated,
}
}
pub fn to_byte(self) -> u8 { self as u8 }
pub fn name(self) -> &'static str {
match self {
DeployTier::Live => "live",
DeployTier::Monitored => "monitored",
DeployTier::HumanGated => "human_gated",
}
}
}
#[derive(Debug, Clone)]
pub struct ImportResult {
pub imported: Vec<TransportTile>,
pub rejected: Vec<RejectedTile>,
pub total_bytes: usize,
pub chunks_processed: usize,
}
#[derive(Debug, Clone)]
pub struct RejectedTile {
pub reason: String,
pub raw_line: String,
}
#[derive(Debug, Clone)]
pub struct CurrentAdapter {
tiles: HashMap<String, TransportTile>,
max_transport: usize, imported_count: u64,
exported_count: u64,
rejected_count: u64,
}
impl CurrentAdapter {
pub fn new() -> Self {
Self {
tiles: HashMap::new(),
max_transport: 1024 * 1024, imported_count: 0,
exported_count: 0,
rejected_count: 0,
}
}
pub fn with_max_transport(mut self, max: usize) -> Self {
self.max_transport = max;
self
}
pub fn store(&mut self, tile: TransportTile) -> bool {
self.tiles.insert(tile.id.clone(), tile).is_none()
}
pub fn get(&self, id: &str) -> Option<&TransportTile> {
self.tiles.get(id)
}
pub fn remove(&mut self, id: &str) -> Option<TransportTile> {
self.tiles.remove(id)
}
pub fn tile_count(&self) -> usize { self.tiles.len() }
pub fn export_tiles(&self, ids: &[&str]) -> ExportResult {
let mut lines = Vec::new();
let mut exported = 0;
let mut missing = Vec::new();
for id in ids {
if let Some(tile) = self.tiles.get(*id) {
let line = format!("TILE {} {} {} {} {} {}",
tile.id,
tile.content,
tile.source,
tile.weight,
tile.tier.to_byte(),
tile.created_at
);
lines.push(line);
exported += 1;
} else {
missing.push(id.to_string());
}
}
let header = format!("PLATO_CURRENT|{}|{}", exported, lines.len());
let mut payload = header;
for line in &lines {
payload.push('\n');
payload.push_str(line);
}
ExportResult {
payload: payload.into_bytes(),
tiles_exported: exported,
missing_ids: missing,
}
}
pub fn import_tiles(&mut self, data: &[u8]) -> ImportResult {
let text = match String::from_utf8(data.to_vec()) {
Ok(t) => t,
Err(_) => return ImportResult {
imported: Vec::new(),
rejected: vec![RejectedTile { reason: "invalid utf8".to_string(), raw_line: String::new() }],
total_bytes: data.len(),
chunks_processed: 0,
},
};
let mut imported = Vec::new();
let mut rejected = Vec::new();
let mut chunks = 0;
for line in text.lines() {
if line.starts_with("PLATO_CURRENT|") {
chunks += 1;
continue;
}
if !line.starts_with("TILE\t") {
rejected.push(RejectedTile {
reason: "missing TILE prefix".to_string(),
raw_line: line.to_string(),
});
self.rejected_count += 1;
continue;
}
let parts: Vec<&str> = line.splitn(7, '\t').collect();
if parts.len() < 6 {
rejected.push(RejectedTile {
reason: format!("expected 6 fields, got {}", parts.len()),
raw_line: line.to_string(),
});
self.rejected_count += 1;
continue;
}
let tile = TransportTile {
id: parts[1].to_string(),
content: parts[2].to_string(),
source: parts[3].to_string(),
weight: parts[4].parse().unwrap_or(0.0),
tier: DeployTier::from_byte(parts[5].parse().unwrap_or(2)),
created_at: parts.get(6).and_then(|s| s.parse().ok()).unwrap_or(0),
};
if tile.id.is_empty() {
rejected.push(RejectedTile {
reason: "empty tile id".to_string(),
raw_line: line.to_string(),
});
self.rejected_count += 1;
continue;
}
let id = tile.id.clone();
self.tiles.insert(id, tile.clone());
imported.push(tile);
self.imported_count += 1;
}
ImportResult {
imported,
rejected,
total_bytes: data.len(),
chunks_processed: chunks,
}
}
pub fn export_all(&self) -> ExportResult {
let ids: Vec<&str> = self.tiles.keys().map(|s| s.as_str()).collect();
self.export_tiles(&ids)
}
pub fn stats(&self) -> CurrentStats {
CurrentStats {
tile_count: self.tiles.len(),
imported_count: self.imported_count,
exported_count: self.exported_count,
rejected_count: self.rejected_count,
max_transport: self.max_transport,
}
}
}
#[derive(Debug, Clone)]
pub struct ExportResult {
pub payload: Vec<u8>,
pub tiles_exported: usize,
pub missing_ids: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct CurrentStats {
pub tile_count: usize,
pub imported_count: u64,
pub exported_count: u64,
pub rejected_count: u64,
pub max_transport: usize,
}
impl Default for CurrentAdapter {
fn default() -> Self { Self::new() }
}
impl CurrentLayer for CurrentAdapter {
fn export(&self, tile_ids: &[&str]) -> Vec<u8> {
let result = self.export_tiles(tile_ids);
result.payload
}
fn import(&mut self, data: &[u8]) -> ImportResult {
self.import_tiles(data)
}
fn transport_capacity(&self) -> usize {
self.max_transport
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_tile(id: &str, content: &str) -> TransportTile {
TransportTile {
id: id.to_string(),
content: content.to_string(),
source: "test".to_string(),
weight: 0.8,
tier: DeployTier::Live,
created_at: 12345,
}
}
#[test]
fn test_store_and_get() {
let mut cur = CurrentAdapter::new();
cur.store(sample_tile("tile-1", "hello"));
let tile = cur.get("tile-1").unwrap();
assert_eq!(tile.content, "hello");
}
#[test]
fn test_store_overwrite() {
let mut cur = CurrentAdapter::new();
assert!(cur.store(sample_tile("tile-1", "v1")));
assert!(!cur.store(sample_tile("tile-1", "v2"))); assert_eq!(cur.get("tile-1").unwrap().content, "v2");
}
#[test]
fn test_remove() {
let mut cur = CurrentAdapter::new();
cur.store(sample_tile("tile-1", "hello"));
assert!(cur.remove("tile-1").is_some());
assert!(cur.get("tile-1").is_none());
}
#[test]
fn test_export_import_roundtrip() {
let mut cur = CurrentAdapter::new();
cur.store(sample_tile("tile-1", "hello world"));
cur.store(sample_tile("tile-2", "foo|bar"));
let exported = cur.export_tiles(&["tile-1", "tile-2"]);
assert_eq!(exported.tiles_exported, 2);
let mut cur2 = CurrentAdapter::new();
let result = cur2.import_tiles(&exported.payload);
assert_eq!(result.imported.len(), 2);
assert!(result.rejected.is_empty());
assert_eq!(cur2.get("tile-1").unwrap().content, "hello world");
assert_eq!(cur2.get("tile-2").unwrap().content, "foo|bar");
}
#[test]
fn test_export_missing_ids() {
let mut cur = CurrentAdapter::new();
cur.store(sample_tile("tile-1", "exists"));
let result = cur.export_tiles(&["tile-1", "tile-999"]);
assert_eq!(result.tiles_exported, 1);
assert_eq!(result.missing_ids, vec!["tile-999"]);
}
#[test]
fn test_import_empty() {
let mut cur = CurrentAdapter::new();
let result = cur.import_tiles(b"");
assert!(result.imported.is_empty());
assert!(result.rejected.is_empty());
}
#[test]
fn test_import_invalid_utf8() {
let mut cur = CurrentAdapter::new();
let bad: Vec<u8> = vec![0xFF, 0xFE];
let result = cur.import_tiles(&bad);
assert!(result.imported.is_empty());
assert_eq!(result.rejected.len(), 1);
}
#[test]
fn test_import_missing_prefix() {
let mut cur = CurrentAdapter::new();
let result = cur.import_tiles(b"not_a_tile_line\nTILE\ta\tb\tc\t0.5\t0\t100");
assert_eq!(result.rejected.len(), 1);
assert_eq!(result.imported.len(), 1);
}
#[test]
fn test_import_empty_id() {
let mut cur = CurrentAdapter::new();
let result = cur.import_tiles(b"TILE\t\tcontent\tsrc\t0.5\t0\t100");
assert!(result.imported.is_empty());
assert_eq!(result.rejected.len(), 1);
}
#[test]
fn test_import_malformed_field_count() {
let mut cur = CurrentAdapter::new();
let result = cur.import_tiles(b"TILE\tonly_two_fields");
assert_eq!(result.rejected.len(), 1);
assert!(result.rejected[0].reason.contains("expected 6 fields"));
}
#[test]
fn test_deploy_tier() {
assert_eq!(DeployTier::from_byte(0), DeployTier::Live);
assert_eq!(DeployTier::from_byte(1), DeployTier::Monitored);
assert_eq!(DeployTier::from_byte(2), DeployTier::HumanGated);
assert_eq!(DeployTier::from_byte(99), DeployTier::HumanGated);
assert_eq!(DeployTier::Live.to_byte(), 0);
}
#[test]
fn test_export_all() {
let mut cur = CurrentAdapter::new();
cur.store(sample_tile("a", "1"));
cur.store(sample_tile("b", "2"));
cur.store(sample_tile("c", "3"));
let result = cur.export_all();
assert_eq!(result.tiles_exported, 3);
assert!(result.missing_ids.is_empty());
}
#[test]
fn test_tab_protocol() {
let line = "TILE\tid\tcontent with | pipes\tsrc\t0.5\t0\t100";
let parts: Vec<&str> = line.splitn(7, '\t').collect();
assert_eq!(parts.len(), 7);
assert_eq!(parts[2], "content with | pipes");
}
#[test]
fn test_roundtrip_with_pipes() {
let mut cur = CurrentAdapter::new();
cur.store(TransportTile {
id: "pipe-test".to_string(),
content: "has|pipes\\and|backslashes".to_string(),
source: "src|with|pipes".to_string(),
weight: 0.9,
tier: DeployTier::Monitored,
created_at: 999,
});
let exported = cur.export_tiles(&["pipe-test"]);
let mut cur2 = CurrentAdapter::new();
let result = cur2.import_tiles(&exported.payload);
assert_eq!(result.imported.len(), 1);
let tile = &result.imported[0];
assert_eq!(tile.content, "has|pipes\\and|backslashes");
assert_eq!(tile.source, "src|with|pipes");
assert_eq!(tile.tier, DeployTier::Monitored);
}
#[test]
fn test_transport_capacity() {
let cur = CurrentAdapter::new().with_max_transport(512);
assert_eq!(cur.transport_capacity(), 512);
}
#[test]
fn test_current_layer_trait() {
let mut cur = CurrentAdapter::new();
cur.store(sample_tile("t1", "data"));
let payload = cur.export(&["t1"]);
assert!(!payload.is_empty());
let mut cur2 = CurrentAdapter::new();
let result = cur2.import(&payload);
assert_eq!(result.imported.len(), 1);
}
#[test]
fn test_stats() {
let mut cur = CurrentAdapter::new();
cur.store(sample_tile("a", "1"));
cur.export_tiles(&["a"]);
cur.import_tiles(b"TILE\tb\tcontent\tsrc\t0.5\t0\t100");
let stats = cur.stats();
assert_eq!(stats.tile_count, 2); assert!(stats.imported_count > 0);
}
}