1
pub(crate) mod dataset;
2
mod fetch;
3
use self::{
4
    dataset::{load_datasets, load_prefixes, 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 sophia::api::prefix::PrefixMapPair;
19
use std::{
20
    error::Error,
21
    path::PathBuf,
22
    sync::{Arc, RwLock},
23
};
24

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

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

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

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

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

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

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

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

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

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

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

            
157
type SharedState = Arc<RwLock<AppState>>;
158

            
159
struct AppState {
160
    config: ServerConfig,
161
    datasets: Vec<Dataset>,
162
    prefixes: Vec<PrefixMapPair>,
163
}
164

            
165
17
fn init_dataset(config: &ServerConfig) -> Result<(), Box<dyn Error>> {
166
17
    let dir = PathBuf::from("datasets");
167
34
    for c in &config.datasets {
168
17
        std::fs::create_dir_all(dir.join(&c.name))?;
169
    }
170
17
    Ok(())
171
17
}
172

            
173
pub fn create_app(
174
    config: ServerConfig,
175
) -> Result<axum::routing::IntoMakeService<Router>, Box<dyn Error>> {
176
17
    init_dataset(&config)?;
177
17
    let datasets = load_datasets("datasets")?;
178
17
    let prefixes = load_prefixes("datasets")?;
179
17
    let shared_state = Arc::new(RwLock::new(AppState {
180
17
        datasets,
181
17
        prefixes,
182
17
        config,
183
17
    }));
184
17
    Ok(Router::new()
185
17
        .route("/", get(route_get_index))
186
17
        .route("/dataset/", get(route_get_index))
187
17
        .route("/dataset/:id", get(route_get_dataset))
188
17
        .route("/dataset/:id/:version", get(route_get_dataset_version))
189
17
        .route(
190
17
            "/dataset/:id/:version/:subject",
191
17
            get(route_get_dataset_version_subject),
192
17
        )
193
17
        .route("/crawl", get(route_get_crawl))
194
17
        .with_state(Arc::clone(&shared_state))
195
17
        .into_make_service())
196
17
}
197

            
198
#[cfg(test)]
199
mod test {
200
    use std::net::SocketAddr;
201

            
202
    use crate::server_config::{ServerConfig, ServerConfigDataset};
203
    use axum::Router;
204
    use axum_test::TestServer;
205
    use tokio::task::{self, JoinHandle};
206
    use tower_http::services::ServeDir;
207

            
208
1
    async fn create_file_server() -> JoinHandle<()> {
209
1
        let app =
210
1
            Router::new().nest_service("/", ServeDir::new("datasets/a/2016-12-19T16:39:57-08:00"));
211
1
        let addr = SocketAddr::from(([127, 0, 0, 1], 8921));
212
1
        let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
213
1
        task::spawn(async {
214
5
            axum::serve(listener, app).await.unwrap();
215
1
        })
216
1
    }
217

            
218
7
    async fn create_server() -> TestServer {
219
7
        let my_app = super::create_app(ServerConfig {
220
7
            datasets: vec![ServerConfigDataset {
221
7
                name: "a".to_string(),
222
7
                url: "http://localhost:8921/index.ttl".to_string(),
223
7
            }],
224
7
        })
225
7
        .unwrap();
226
7
        let server = TestServer::new(my_app).unwrap();
227
7
        server
228
7
    }
229

            
230
1
    #[tokio::test]
231
1
    async fn route_root() {
232
1
        let server = create_server().await;
233
1
        let response = server.get("/").await;
234
1
        assert_eq!(response.status_code(), 200);
235
    }
236
1
    #[tokio::test]
237
1
    async fn route_dataset() {
238
1
        let server = create_server().await;
239
1
        let response = server.get("/dataset/").await;
240
1
        assert_eq!(response.status_code(), 200);
241
    }
242
1
    #[tokio::test]
243
1
    async fn route_dataset_a() {
244
1
        let server = create_server().await;
245
1
        let response = server.get("/dataset/a").await;
246
1
        assert_eq!(response.status_code(), 200);
247
    }
248
1
    #[tokio::test]
249
1
    async fn route_dataset_a_date() {
250
1
        let server = create_server().await;
251
1
        let response = server.get("/dataset/a/2016-12-20T00:39:57Z").await;
252
1
        assert_eq!(response.status_code(), 200);
253
    }
254
1
    #[tokio::test]
255
1
    async fn route_dataset_a_date_subject() {
256
1
        let server = create_server().await;
257
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;
258
1
        assert_eq!(response.status_code(), 200);
259
    }
260
1
    #[tokio::test]
261
1
    async fn route_crawl() {
262
1
        let handle = create_file_server().await;
263
1
        let server = create_server().await;
264
28
        let response = server.get("/crawl").await;
265
1
        assert_eq!(response.status_code(), 200);
266
1
        handle.abort();
267
    }
268
1
    #[tokio::test]
269
1
    async fn route_dataset_b() {
270
1
        let server = create_server().await;
271
1
        let response = server.get("/dataset/b").await;
272
1
        assert_eq!(response.status_code(), 404);
273
    }
274
}