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

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

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

            
41
2
async fn route_get_index(State(state): State<SharedState>) -> Result<Html<String>, MyError> {
42
2
    let state = &mut state.read().unwrap();
43
2
    let datasets: Vec<_> = state.datasets.iter().map(DatasetSummary::from).collect();
44
2
    let template = IndexTemplate {
45
2
        datasets: &datasets,
46
2
    };
47
2
    Ok(Html(template.render()?))
48
2
}
49

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

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

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

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

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

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

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

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

            
152
type SharedState = Arc<RwLock<AppState>>;
153

            
154
struct AppState {
155
    config: ServerConfig,
156
    datasets: Vec<Dataset>,
157
}
158

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

            
167
pub fn create_app(
168
    config: ServerConfig,
169
) -> Result<axum::routing::IntoMakeService<Router>, Box<dyn Error>> {
170
17
    init_dataset(&config)?;
171
17
    let datasets = load_datasets("datasets")?;
172
17
    let shared_state = Arc::new(RwLock::new(AppState { datasets, config }));
173
17
    Ok(Router::new()
174
17
        .route("/", get(route_get_index))
175
17
        .route("/dataset/", get(route_get_index))
176
17
        .route("/dataset/:id", get(route_get_dataset))
177
17
        .route("/dataset/:id/:version", get(route_get_dataset_version))
178
17
        .route(
179
17
            "/dataset/:id/:version/:subject",
180
17
            get(route_get_dataset_version_subject),
181
17
        )
182
17
        .route("/crawl", get(route_get_crawl))
183
17
        .with_state(Arc::clone(&shared_state))
184
17
        .into_make_service())
185
17
}
186

            
187
#[cfg(test)]
188
mod test {
189
    use std::net::SocketAddr;
190

            
191
    use crate::server_config::{ServerConfig, ServerConfigDataset};
192
    use axum::Router;
193
    use axum_test::TestServer;
194
    use tokio::task::{self, JoinHandle};
195
    use tower_http::services::ServeDir;
196

            
197
1
    async fn create_file_server() -> JoinHandle<()> {
198
1
        let app =
199
1
            Router::new().nest_service("/", ServeDir::new("datasets/a/2016-12-19T16:39:57-08:00"));
200
1
        let addr = SocketAddr::from(([127, 0, 0, 1], 8921));
201
1
        let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
202
1
        task::spawn(async {
203
5
            axum::serve(listener, app).await.unwrap();
204
1
        })
205
1
    }
206

            
207
7
    async fn create_server() -> TestServer {
208
7
        let my_app = super::create_app(ServerConfig {
209
7
            datasets: vec![ServerConfigDataset {
210
7
                name: "a".to_string(),
211
7
                url: "http://localhost:8921/index.ttl".to_string(),
212
7
            }],
213
7
        })
214
7
        .unwrap();
215
7
        let server = TestServer::new(my_app).unwrap();
216
7
        server
217
7
    }
218

            
219
1
    #[tokio::test]
220
1
    async fn route_root() {
221
1
        let server = create_server().await;
222
1
        let response = server.get("/").await;
223
1
        assert_eq!(response.status_code(), 200);
224
    }
225
1
    #[tokio::test]
226
1
    async fn route_dataset() {
227
1
        let server = create_server().await;
228
1
        let response = server.get("/dataset/").await;
229
1
        assert_eq!(response.status_code(), 200);
230
    }
231
1
    #[tokio::test]
232
1
    async fn route_dataset_a() {
233
1
        let server = create_server().await;
234
1
        let response = server.get("/dataset/a").await;
235
1
        assert_eq!(response.status_code(), 200);
236
    }
237
1
    #[tokio::test]
238
1
    async fn route_dataset_a_date() {
239
1
        let server = create_server().await;
240
1
        let response = server.get("/dataset/a/2016-12-20T00:39:57Z").await;
241
1
        assert_eq!(response.status_code(), 200);
242
    }
243
1
    #[tokio::test]
244
1
    async fn route_dataset_a_date_subject() {
245
1
        let server = create_server().await;
246
1
        let response = server.get("/dataset/a/2016-12-20T00:39:57Z/http%3A%2F%2Fwww.w3.org%2F1999%2F02%2F22-rdf-syntax-ns%23").await;
247
1
        assert_eq!(response.status_code(), 200);
248
    }
249
1
    #[tokio::test]
250
1
    async fn route_crawl() {
251
1
        let handle = create_file_server().await;
252
1
        let server = create_server().await;
253
28
        let response = server.get("/crawl").await;
254
1
        assert_eq!(response.status_code(), 200);
255
1
        handle.abort();
256
    }
257
1
    #[tokio::test]
258
1
    async fn route_dataset_b() {
259
1
        let server = create_server().await;
260
1
        let response = server.get("/dataset/b").await;
261
1
        assert_eq!(response.status_code(), 404);
262
    }
263
}