Lines
96.72 %
Functions
69.7 %
pub(crate) mod dataset;
mod fetch;
use self::{
dataset::{load_datasets, load_prefixes, Dataset, DatasetVersion},
fetch::fetch_all,
};
use crate::templates::*;
use crate::{server_config::ServerConfig, server_error::MyError};
use askama::Template;
use axum::{
extract::{Path, State},
response::Html,
routing::get,
Router,
use chrono::{DateTime, Utc};
use serde::Serialize;
use sophia::api::prefix::PrefixMapPair;
use std::{
error::Error,
path::PathBuf,
sync::{Arc, RwLock},
#[derive(Serialize)]
pub(crate) struct DatasetSummary<'a> {
pub id: &'a str,
pub latest_version: Option<DateTime<Utc>>,
pub number_of_versions: usize,
}
impl<'a> From<&'a Dataset> for DatasetSummary<'a> {
fn from(dataset: &'a Dataset) -> Self {
DatasetSummary {
id: &dataset.id,
latest_version: dataset.latest_version(),
number_of_versions: dataset.versions.len(),
async fn route_get_index(State(state): State<SharedState>) -> Result<Html<String>, MyError> {
let state = &mut state.read().unwrap();
let datasets: Vec<_> = state.datasets.iter().map(DatasetSummary::from).collect();
let template = IndexTemplate {
datasets: &datasets,
Ok(Html(template.render()?))
pub(crate) struct DatasetVersionSummary {
pub date: DateTime<Utc>,
pub number_of_quads: usize,
impl<'a> From<&'a DatasetVersion> for DatasetVersionSummary {
fn from(version: &'a DatasetVersion) -> Self {
DatasetVersionSummary {
date: version.date,
number_of_quads: version.number_of_quads(),
fn get_dataset<'a>(datasets: &'a [Dataset], id: &str) -> Result<&'a Dataset, MyError> {
if let Some(dataset) = datasets.iter().find(|d| d.id == id) {
Ok(dataset)
} else {
Err(MyError::NotFound(format!("No dataset with id {}", id)))
pub(crate) fn get_dataset_version<'a>(
datasets: &'a [Dataset],
id: &str,
date: DateTime<Utc>,
) -> Result<(&'a Dataset, &'a DatasetVersion), MyError> {
let dataset = get_dataset(datasets, id)?;
if let Some(version) = dataset.versions.iter().find(|v| v.date == date) {
Ok((dataset, version))
Err(MyError::NotFound(format!(
"No dataset with id {} and version {}",
id, date
)))
async fn route_get_dataset(
State(state): State<SharedState>,
Path(id): Path<String>,
) -> Result<Html<String>, MyError> {
let dataset = get_dataset(&state.datasets, &id)?;
let versions: Vec<_> = dataset
.versions
.iter()
.map(DatasetVersionSummary::from)
.collect();
let template = DatasetTemplate {
dataset,
versions: &versions,
async fn route_get_dataset_version(
Path((id, version)): Path<(String, DateTime<Utc>)>,
let (dataset, version) = get_dataset_version(&state.datasets, &id, version)?;
let subjects = version.subjects();
let dataset = DatasetSummary::from(dataset);
let version = DatasetVersionSummary::from(version);
let template = VersionTemplate {
dataset: &dataset,
version: &version,
subjects,
datasets: &state.datasets,
prefixes: &state.prefixes,
async fn route_get_dataset_version_subject(
Path((id, version, subject)): Path<(String, DateTime<Utc>, String)>,
let pos = version.pos(&subject);
let template = SubjectTemplate {
subject,
pos,
async fn route_get_crawl(State(state): State<SharedState>) -> Result<Html<String>, MyError> {
let dataset_configs: Vec<_>;
{
dataset_configs = state.config.datasets.clone();
fetch_all(dataset_configs, &PathBuf::from("datasets")).await?;
Ok(Html("Ok".into()))
type SharedState = Arc<RwLock<AppState>>;
struct AppState {
config: ServerConfig,
datasets: Vec<Dataset>,
prefixes: Vec<PrefixMapPair>,
fn init_dataset(config: &ServerConfig) -> Result<(), Box<dyn Error>> {
let dir = PathBuf::from("datasets");
for c in &config.datasets {
std::fs::create_dir_all(dir.join(&c.name))?;
Ok(())
pub fn create_app(
) -> Result<axum::routing::IntoMakeService<Router>, Box<dyn Error>> {
init_dataset(&config)?;
let datasets = load_datasets("datasets")?;
let prefixes = load_prefixes("datasets")?;
let shared_state = Arc::new(RwLock::new(AppState { datasets, prefixes, config }));
Ok(Router::new()
.route("/", get(route_get_index))
.route("/dataset/", get(route_get_index))
.route("/dataset/:id", get(route_get_dataset))
.route("/dataset/:id/:version", get(route_get_dataset_version))
.route(
"/dataset/:id/:version/:subject",
get(route_get_dataset_version_subject),
)
.route("/crawl", get(route_get_crawl))
.with_state(Arc::clone(&shared_state))
.into_make_service())
#[cfg(test)]
mod test {
use std::net::SocketAddr;
use crate::server_config::{ServerConfig, ServerConfigDataset};
use axum::Router;
use axum_test::TestServer;
use tokio::task::{self, JoinHandle};
use tower_http::services::ServeDir;
async fn create_file_server() -> JoinHandle<()> {
let app =
Router::new().nest_service("/", ServeDir::new("datasets/a/2016-12-19T16:39:57-08:00"));
let addr = SocketAddr::from(([127, 0, 0, 1], 8921));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
task::spawn(async {
axum::serve(listener, app).await.unwrap();
})
async fn create_server() -> TestServer {
let my_app = super::create_app(ServerConfig {
datasets: vec![ServerConfigDataset {
name: "a".to_string(),
url: "http://localhost:8921/index.ttl".to_string(),
}],
.unwrap();
let server = TestServer::new(my_app).unwrap();
server
#[tokio::test]
async fn route_root() {
let server = create_server().await;
let response = server.get("/").await;
assert_eq!(response.status_code(), 200);
async fn route_dataset() {
let response = server.get("/dataset/").await;
async fn route_dataset_a() {
let response = server.get("/dataset/a").await;
async fn route_dataset_a_date() {
let response = server.get("/dataset/a/2016-12-20T00:39:57Z").await;
async fn route_dataset_a_date_subject() {
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;
async fn route_crawl() {
let handle = create_file_server().await;
let response = server.get("/crawl").await;
handle.abort();
async fn route_dataset_b() {
let response = server.get("/dataset/b").await;
assert_eq!(response.status_code(), 404);