1
mod dataset;
2
mod fetch;
3
use self::{
4
    dataset::{load_datasets, Dataset, DatasetVersion},
5
    fetch::fetch_all,
6
};
7
use crate::{server_config::ServerConfig, server_error::MyError};
8
use axum::{
9
    body::HttpBody,
10
    extract::{Path, State},
11
    response::Html,
12
    routing::get,
13
    Router,
14
};
15
use chrono::{DateTime, Utc};
16
use serde::Serialize;
17
use std::{
18
    error::Error,
19
    path::PathBuf,
20
    sync::{Arc, RwLock},
21
};
22
use tera::{Context, Tera};
23

            
24
#[derive(Serialize)]
25
struct DatasetSummary<'a> {
26
    id: &'a str,
27
    latest_version: Option<DateTime<Utc>>,
28
    number_of_versions: usize,
29
}
30

            
31
impl<'a> From<&'a Dataset> for DatasetSummary<'a> {
32
    fn from(dataset: &'a Dataset) -> Self {
33
        DatasetSummary {
34
            id: &dataset.id,
35
            latest_version: dataset.latest_version(),
36
            number_of_versions: dataset.versions.len(),
37
        }
38
    }
39
}
40

            
41
1
async fn route_get_index(State(state): State<SharedState>) -> Result<Html<String>, MyError> {
42
1
    let state = &mut state.read().unwrap();
43
1
    let mut context = Context::new();
44
1
    let datasets: Vec<_> = state.datasets.iter().map(DatasetSummary::from).collect();
45
1
    context.insert("datasets", &datasets);
46
1
    let html = state.tera.render("index.html", &context)?;
47
1
    Ok(Html(html))
48
1
}
49

            
50
#[derive(Serialize)]
51
struct DatasetVersionSummary {
52
    date: DateTime<Utc>,
53
    number_of_quads: usize,
54
}
55

            
56
impl<'a> From<&'a DatasetVersion> for DatasetVersionSummary {
57
    fn from(version: &'a DatasetVersion) -> Self {
58
        DatasetVersionSummary {
59
            date: version.date,
60
            number_of_quads: version.number_of_quads(),
61
        }
62
    }
63
}
64

            
65
fn get_dataset<'a>(datasets: &'a [Dataset], id: &str) -> Result<&'a Dataset, MyError> {
66
    if let Some(dataset) = datasets.iter().find(|d| d.id == id) {
67
        Ok(dataset)
68
    } else {
69
        Err(MyError::NotFound(format!("No dataset with id {}", id)))
70
    }
71
}
72

            
73
fn get_dataset_version<'a>(
74
    datasets: &'a [Dataset],
75
    id: &str,
76
    date: DateTime<Utc>,
77
) -> Result<(&'a Dataset, &'a DatasetVersion), MyError> {
78
    let dataset = get_dataset(datasets, id)?;
79
    if let Some(version) = dataset.versions.iter().find(|v| v.date == date) {
80
        Ok((dataset, version))
81
    } else {
82
        Err(MyError::NotFound(format!(
83
            "No dataset with id {} and version {}",
84
            id, date
85
        )))
86
    }
87
}
88

            
89
async fn route_get_dataset(
90
    State(state): State<SharedState>,
91
    Path(id): Path<String>,
92
) -> Result<Html<String>, MyError> {
93
    let state = &mut state.read().unwrap();
94
    let dataset = get_dataset(&state.datasets, &id)?;
95
    let mut context = Context::new();
96
    let summary = DatasetSummary::from(dataset);
97
    context.insert("dataset", &summary);
98
    let versions: Vec<_> = dataset
99
        .versions
100
        .iter()
101
        .map(DatasetVersionSummary::from)
102
        .collect();
103
    context.insert("versions", &versions);
104
    Ok(Html(state.tera.render("dataset.html", &context)?))
105
}
106

            
107
async fn route_get_dataset_version(
108
    State(state): State<SharedState>,
109
    Path((id, version)): Path<(String, DateTime<Utc>)>,
110
) -> Result<Html<String>, MyError> {
111
    let state = &mut state.read().unwrap();
112
    let (dataset, version) = get_dataset_version(&state.datasets, &id, version)?;
113
    let subjects = version.subjects();
114
    let dataset = DatasetSummary::from(dataset);
115
    let version = DatasetVersionSummary::from(version);
116
    let mut context = Context::new();
117
    context.insert("dataset", &dataset);
118
    context.insert("version", &version);
119
    context.insert("subjects", &subjects);
120
    Ok(Html(state.tera.render("version.html", &context)?))
121
}
122

            
123
async fn route_get_dataset_version_subject(
124
    State(state): State<SharedState>,
125
    Path((id, version, subject)): Path<(String, DateTime<Utc>, String)>,
126
) -> Result<Html<String>, MyError> {
127
    let state = &mut state.read().unwrap();
128
    let (dataset, version) = get_dataset_version(&state.datasets, &id, version)?;
129
    let pos = version.pos(&subject);
130
    let dataset = DatasetSummary::from(dataset);
131
    let version = DatasetVersionSummary::from(version);
132
    let mut context = Context::new();
133
    context.insert("dataset", &dataset);
134
    context.insert("version", &version);
135
    context.insert("subject", &subject);
136
    context.insert("pos", &pos);
137
    Ok(Html(state.tera.render("subject.html", &context)?))
138
}
139

            
140
async fn route_get_crawl(State(state): State<SharedState>) -> Result<Html<String>, MyError> {
141
    let dataset_configs: Vec<_>;
142
    {
143
        let state = &mut state.read().unwrap();
144
        dataset_configs = state.config.datasets.clone();
145
    }
146
    fetch_all(dataset_configs, &PathBuf::from("datasets")).await?;
147
    Ok(Html("Ok".into()))
148
}
149

            
150
type SharedState = Arc<RwLock<AppState>>;
151

            
152
struct AppState {
153
    config: ServerConfig,
154
    datasets: Vec<Dataset>,
155
    tera: Tera,
156
}
157

            
158
1
fn init_dataset(config: &ServerConfig) -> Result<(), Box<dyn Error>> {
159
1
    let dir = PathBuf::from("datasets");
160
1
    for c in &config.datasets {
161
        std::fs::create_dir_all(dir.join(&c.name))?;
162
    }
163
1
    Ok(())
164
1
}
165

            
166
pub fn create_app<B: HttpBody + Send + 'static>(
167
    config: ServerConfig,
168
) -> Result<axum::routing::IntoMakeService<Router<(), B>>, Box<dyn Error>>
169
where
170
    <B as HttpBody>::Error: Sync + Send + std::error::Error,
171
    <B as HttpBody>::Data: Send,
172
{
173
1
    init_dataset(&config)?;
174
1
    let tera = Tera::new("templates/**/[a-zA-Z0-9]*")?;
175
1
    let datasets = load_datasets("datasets")?;
176
1
    let shared_state = Arc::new(RwLock::new(AppState {
177
1
        datasets,
178
1
        tera,
179
1
        config,
180
1
    }));
181
1
    Ok(Router::new()
182
1
        .route("/", get(route_get_index))
183
1
        .route("/dataset/", get(route_get_index))
184
1
        .route("/dataset/:id", get(route_get_dataset))
185
1
        .route("/dataset/:id/:version", get(route_get_dataset_version))
186
1
        .route(
187
1
            "/dataset/:id/:version/:subject",
188
1
            get(route_get_dataset_version_subject),
189
1
        )
190
1
        .route("/crawl", get(route_get_crawl))
191
1
        .with_state(Arc::clone(&shared_state))
192
1
        .into_make_service())
193
1
}
194

            
195
#[cfg(test)]
196
mod test {
197
    use axum_test::TestServer;
198

            
199
    use crate::server_config::ServerConfig;
200

            
201
1
    #[tokio::test]
202
1
    async fn create_server() {
203
1
        let my_app = super::create_app(ServerConfig { datasets: vec![] }).unwrap();
204
1
        let server = TestServer::new(my_app).unwrap();
205
3
        let response = server.get("/").await;
206
1
        assert_eq!(response.status_code(), 200);
207
    }
208
}