1use std::collections::BTreeMap;
14
15use serde::{Deserialize, Serialize};
16
17const BUILTIN: &str = include_str!("../data/survival.json");
18
19#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct Layer {
21 pub key: String,
22 pub label: String,
23 pub description: String,
24}
25
26#[derive(Debug, Clone, Deserialize, Serialize)]
27pub struct Cell {
28 pub verdict: String,
29 pub evidence: String,
30 pub note: String,
31 #[serde(default)]
32 pub sources: Vec<Source>,
33}
34
35#[derive(Debug, Clone, Deserialize, Serialize)]
36pub struct Source {
37 pub title: String,
38 pub url: String,
39}
40
41#[derive(Debug, Clone, Deserialize, Serialize)]
42pub struct Symbols {
43 pub verdict: BTreeMap<String, String>,
44 pub evidence: BTreeMap<String, String>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct Table {
49 pub researched: String,
50 pub advice: String,
51 pub how_to_verify: String,
52 pub legend: BTreeMap<String, String>,
53 pub symbols: Symbols,
54 pub layers: Vec<Layer>,
55 pub platforms: BTreeMap<String, BTreeMap<String, Cell>>,
58}
59
60pub const PLATFORM_ORDER: [&str; 7] = [
63 "LinkedIn",
64 "Instagram",
65 "Facebook",
66 "X / Twitter",
67 "Reddit",
68 "Printables",
69 "Google Images",
70];
71
72impl Table {
73 pub fn load() -> Table {
74 if let Some(path) = std::env::var_os("SNITCH_SURVIVAL") {
77 match std::fs::read_to_string(&path)
78 .map_err(|e| e.to_string())
79 .and_then(|s| serde_json::from_str::<Table>(&s).map_err(|e| e.to_string()))
80 {
81 Ok(table) => return table,
82 Err(e) => eprintln!(
83 " SNITCH_SURVIVAL at {} could not be read, using the built-in table: {e}",
84 std::path::Path::new(&path).display()
85 ),
86 }
87 }
88 serde_json::from_str(BUILTIN).expect("the built-in survival table is valid JSON")
89 }
90
91 pub fn ordered_platforms(&self) -> Vec<(&String, &BTreeMap<String, Cell>)> {
92 let mut out: Vec<_> = PLATFORM_ORDER
93 .iter()
94 .filter_map(|name| self.platforms.get_key_value(*name))
95 .collect();
96 for (name, layers) in &self.platforms {
98 if !PLATFORM_ORDER.contains(&name.as_str()) {
99 out.push((name, layers));
100 }
101 }
102 out
103 }
104
105 pub fn display_cell(&self, cell: &Cell) -> String {
106 let evidence = self
107 .symbols
108 .evidence
109 .get(&cell.evidence)
110 .map(String::as_str)
111 .unwrap_or("?");
112 let verdict = self
113 .symbols
114 .verdict
115 .get(&cell.verdict)
116 .map(String::as_str)
117 .unwrap_or(cell.verdict.as_str());
118 format!("{evidence} {verdict}")
119 }
120
121 pub fn as_json(&self, include_notes: bool, include_check: bool) -> serde_json::Value {
123 let mut platforms = serde_json::Map::new();
124 for (name, layers) in self.ordered_platforms() {
125 let mut entry = serde_json::Map::new();
126 for layer in &self.layers {
127 let Some(cell) = layers.get(&layer.key) else {
128 continue;
129 };
130 let mut item = serde_json::json!({
131 "verdict": cell.verdict,
132 "evidence": cell.evidence,
133 "live_tested": cell.evidence == "corroborated",
134 });
135 if include_notes {
136 item["note"] = serde_json::json!(cell.note);
137 item["sources"] = serde_json::to_value(&cell.sources).unwrap_or_default();
138 }
139 entry.insert(layer.key.clone(), item);
140 }
141 platforms.insert(name.clone(), serde_json::Value::Object(entry));
142 }
143 let mut report = serde_json::json!({
146 "researched": self.researched,
147 "legend": {
148 "D": self.legend.get("D"),
149 "C": self.legend.get("C"),
150 "?": self.legend.get("?"),
151 },
152 "layers": self.layers,
153 "platforms": platforms,
154 "advice": self.advice,
155 });
156 if include_check {
157 report["how_to_verify"] = serde_json::json!(self.how_to_verify);
158 }
159 report
160 }
161}