use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TileBounds {
pub max_deviation: f64,
pub max_age: u32,
pub min_agreement: usize,
}
impl TileBounds {
pub fn new(max_deviation: f64, max_age: u32, min_agreement: usize) -> Self {
Self { max_deviation, max_age, min_agreement }
}
pub fn check_bounds(&self, deviation: f64) -> bool {
deviation <= self.max_deviation
}
pub fn default_fleet() -> Self {
Self {
max_deviation: 1.0,
max_age: 100,
min_agreement: 3, }
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FleetTile {
pub room: String,
pub question: String,
pub answer: String,
pub domain: Option<String>,
pub tags: Vec<String>,
pub author: String,
pub timestamp: f64,
}
impl FleetTile {
pub fn new(room: String, question: String, answer: String) -> Self {
Self {
room,
question,
answer,
domain: None,
tags: vec![],
author: "fleet".to_string(),
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as f64)
.unwrap_or(0.0),
}
}
pub fn with_domain(mut self, domain: String) -> Self {
self.domain = Some(domain);
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TileCoordination {
pub room: String,
pub tiles: Vec<FleetTile>,
pub consensus_tiles: Vec<String>,
pub pending_tiles: Vec<String>,
}
impl TileCoordination {
pub fn new(room: String) -> Self {
Self {
room,
tiles: Vec::new(),
consensus_tiles: Vec::new(),
pending_tiles: Vec::new(),
}
}
pub fn add_tile(&mut self, tile: FleetTile) {
if !self.tiles.iter().any(|t| t.question == tile.question) {
self.tiles.push(tile.clone());
self.pending_tiles.push(tile.question.clone());
}
}
pub fn mark_consensus(&mut self, question: &str) {
self.pending_tiles.retain(|q| q != question);
if !self.consensus_tiles.contains(&question.to_string()) {
self.consensus_tiles.push(question.to_string());
}
}
pub fn tile_count(&self) -> usize {
self.tiles.len()
}
}
pub fn tile_to_glue(tile: &FleetTile) -> Vec<u8> {
let msg = serde_json::json!({
"t": 0x05,
"room": tile.room,
"question": tile.question,
"answer": tile.answer,
"author": tile.author,
});
serde_json::to_vec(&msg).unwrap_or_default()
}
pub fn glue_to_tile(data: &[u8]) -> Option<FleetTile> {
let msg: serde_json::Value = serde_json::from_slice(data).ok()?;
Some(FleetTile {
room: msg["room"].as_str()?.to_string(),
question: msg["question"].as_str()?.to_string(),
answer: msg["answer"].as_str()?.to_string(),
domain: msg["domain"].as_str().map(String::from),
tags: msg["tags"].as_array()
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default(),
author: msg["author"].as_str().unwrap_or("fleet").to_string(),
timestamp: msg["timestamp"].as_f64().unwrap_or(0.0),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tile_roundtrip() {
let tile = FleetTile::new(
"fleet_math".to_string(),
"What is the Betti number beta1?".to_string(),
"beta1 = E - V + C for a graph with C connected components.".to_string(),
);
let data = tile_to_glue(&tile);
assert!(!data.is_empty());
}
#[test]
fn test_coordination_mark_consensus() {
let mut coord = TileCoordination::new("test".to_string());
coord.add_tile(FleetTile::new("test".to_string(), "Q1?".to_string(), "A1".to_string()));
coord.add_tile(FleetTile::new("test".to_string(), "Q2?".to_string(), "A2".to_string()));
assert_eq!(coord.tile_count(), 2);
assert_eq!(coord.pending_tiles.len(), 2);
coord.mark_consensus("Q1?");
assert_eq!(coord.consensus_tiles.len(), 1);
assert_eq!(coord.pending_tiles.len(), 1);
}
#[test]
fn test_tilebounds_check_bounds() {
let bounds = TileBounds::new(1.0, 100, 3);
assert!(bounds.check_bounds(0.5));
assert!(bounds.check_bounds(1.0));
assert!(!bounds.check_bounds(1.1));
}
#[test]
fn test_tilebounds_default_fleet() {
let bounds = TileBounds::default_fleet();
assert!(bounds.check_bounds(0.5));
assert_eq!(bounds.max_age, 100);
assert_eq!(bounds.min_agreement, 3);
}
#[test]
fn test_tilebounds_zero_deviation() {
let bounds = TileBounds::new(0.0, 50, 5);
assert!(bounds.check_bounds(0.0));
assert!(!bounds.check_bounds(0.001));
}
#[test]
fn test_tilebounds_exactly_at_bound() {
let bounds = TileBounds::new(2.5, 200, 4);
assert!(bounds.check_bounds(2.5));
assert!(!bounds.check_bounds(2.5001));
}
}