1pub mod api;
4pub mod assets;
5
6use crate::cli::InputFormat;
7use crate::diff::{DiffResult, compute_diff, load_diff_file};
8use anyhow::{Context, Result};
9use std::net::SocketAddr;
10use std::path::PathBuf;
11use std::sync::Arc;
12use tokio::sync::Mutex;
13
14#[derive(Clone, Default)]
16pub struct AppState {
17 pub data: Arc<Mutex<Option<Arc<DiffResult>>>>,
18}
19
20pub struct Server {
22 pub addr: SocketAddr,
23 handle: tokio::task::JoinHandle<()>,
24}
25
26impl Server {
27 pub async fn join(self) -> Result<()> {
28 self.handle.await.context("web server task panicked")?;
29 Ok(())
30 }
31}
32
33pub enum Preload {
35 None,
36 Files {
37 file_a: PathBuf,
38 file_b: PathBuf,
39 format_a: Option<InputFormat>,
40 format_b: Option<InputFormat>,
41 graph_a: Option<String>,
42 graph_b: Option<String>,
43 ignore_blank_nodes: bool,
44 },
45 Diff {
46 diff: PathBuf,
47 format: Option<InputFormat>,
48 graph_a: Option<String>,
49 graph_b: Option<String>,
50 },
51 Loaded(DiffResult),
52}
53
54pub async fn build_state(preload: Preload) -> Result<AppState> {
55 let state = AppState::default();
56 match preload {
57 Preload::None => {}
58 Preload::Loaded(mut d) => {
59 d.sort_rows();
60 *state.data.lock().await = Some(Arc::new(d));
61 }
62 Preload::Files {
63 file_a,
64 file_b,
65 format_a,
66 format_b,
67 graph_a,
68 graph_b,
69 ignore_blank_nodes,
70 } => {
71 let inputs = crate::diff::DiffInputs {
72 file_a,
73 file_b,
74 format_a,
75 format_b,
76 graph_a,
77 graph_b,
78 ignore_blank_nodes,
79 };
80 let mut result = tokio::task::spawn_blocking(move || compute_diff(&inputs))
81 .await
82 .context("diff task panicked")??;
83 result.sort_rows();
84 *state.data.lock().await = Some(Arc::new(result));
85 }
86 Preload::Diff {
87 diff,
88 format,
89 graph_a,
90 graph_b,
91 } => {
92 let inputs = crate::diff::LoadDiffInputs {
93 diff,
94 format,
95 graph_a,
96 graph_b,
97 };
98 let mut result = tokio::task::spawn_blocking(move || load_diff_file(&inputs))
99 .await
100 .context("load task panicked")??;
101 result.sort_rows();
102 *state.data.lock().await = Some(Arc::new(result));
103 }
104 }
105 Ok(state)
106}
107
108pub async fn start(bind: &str, state: AppState) -> Result<Server> {
110 let listener = tokio::net::TcpListener::bind(bind)
111 .await
112 .with_context(|| format!("failed to bind {bind}"))?;
113 let addr = listener.local_addr().context("local_addr")?;
114 let app = api::router(state);
115 let handle = tokio::spawn(async move {
116 if let Err(e) = axum::serve(listener, app).await {
117 eprintln!("web server error: {e}");
118 }
119 });
120 Ok(Server { addr, handle })
121}
122
123pub fn run_blocking(bind: &str, open: bool, preload: Preload) -> Result<()> {
126 let rt = tokio::runtime::Builder::new_current_thread()
127 .enable_all()
128 .build()
129 .context("failed to build tokio runtime")?;
130 rt.block_on(async move {
131 let state = build_state(preload).await?;
132 let server = start(bind, state).await?;
133 let url = format!("http://{}/", server.addr);
134 eprintln!("rdf-compare viewer listening on {url}");
135 if open && let Err(e) = webbrowser::open(&url) {
136 eprintln!("could not open browser: {e}");
137 }
138 tokio::select! {
140 r = tokio::signal::ctrl_c() => {
141 r.context("ctrl-c handler failed")?;
142 eprintln!("shutting down");
143 Ok::<(), anyhow::Error>(())
144 }
145 _ = server.handle => Ok(()),
146 }
147 })
148}