#![warn(missing_docs)]
#![warn(clippy::pedantic)]
#![allow(clippy::wildcard_imports)]
#![allow(clippy::enum_glob_use)]
#![allow(clippy::unused_async)]
#![allow(clippy::similar_names)]
#![deny(rust_2018_idioms)]
#![feature(once_cell)]
mod about;
mod config;
mod rdf;
mod resource;
use crate::config::config;
use about::About;
use actix_web::{get, web, web::scope, App, HttpRequest, HttpResponse, HttpServer, Responder};
use log::{debug, error, info, trace, warn};
use std::time::Instant;
use tinytemplate::TinyTemplate;
static RESOURCE: &str = std::include_str!("../data/resource.html");
static FAVICON: &[u8; 318] = std::include_bytes!("../data/favicon.ico");
static CSS: &str = std::include_str!("../data/rickview.css");
static INDEX: &str = std::include_str!("../data/index.html");
static ABOUT: &str = std::include_str!("../data/about.html");
fn template() -> TinyTemplate<'static> {
let mut tt = TinyTemplate::new();
tt.add_template("resource", RESOURCE).expect("Could not parse resource page template");
tt.add_template("index", INDEX).expect("Could not parse index page template");
tt.add_template("about", ABOUT).expect("Could not parse about page template");
tt.add_formatter("uri_to_suffix", |json, output| {
let o = || -> Option<String> {
let s = json.as_str().unwrap_or_else(|| panic!("JSON value is not a string: {json}"));
let mut s = s.rsplit_once('/').unwrap_or_else(|| panic!("no '/' in URI '{s}'")).1;
if s.contains('#') {
s = s.rsplit_once('#')?.1;
}
Some(s.to_owned())
};
output.push_str(&o().unwrap());
Ok(())
});
tt
}
#[get("{_anypath:.*/|}rickview.css")]
async fn css() -> impl Responder { HttpResponse::Ok().content_type("text/css").body(CSS) }
#[get("{_anypath:.*/|}favicon.ico")]
async fn favicon() -> impl Responder { HttpResponse::Ok().content_type("image/x-icon").body(FAVICON.as_ref()) }
#[get("{suffix:.*|}")]
async fn res_html(request: HttpRequest, suffix: web::Path<String>) -> impl Responder {
let t = Instant::now();
let prefixed = config().prefix.clone() + ":" + &suffix;
match rdf::resource(&suffix) {
Err(_) => {
let message = format!("No triples found for resource {prefixed}");
warn!("{}", message);
HttpResponse::NotFound().content_type("text/plain").body(message)
}
Ok(res) => {
match request.head().headers().get("Accept") {
Some(a) => {
if let Ok(accept) = a.to_str() {
trace!("{} accept header {}", prefixed, accept);
if accept.contains("text/html") {
return match template().render("resource", &res) {
Ok(html) => {
debug!("{} HTML {:?}", prefixed, t.elapsed());
HttpResponse::Ok().content_type("text/html; charset-utf-8").body(html)
}
Err(err) => {
let message = format!("Internal server error. Could not render resource {prefixed}:\n{err}.");
error!("{}", message);
HttpResponse::InternalServerError().body(message)
}
};
}
if accept.contains("application/n-triples") {
debug!("{} N-Triples {:?}", prefixed, t.elapsed());
return HttpResponse::Ok().content_type("application/n-triples").body(rdf::serialize_nt(&suffix));
}
#[cfg(feature = "rdfxml")]
if accept.contains("application/rdf+xml") {
debug!("{} RDF {:?}", prefixed, t.elapsed());
return HttpResponse::Ok().content_type("application/rdf+xml").body(rdf::serialize_rdfxml(&suffix));
}
warn!("{} accept header {} not recognized, using RDF Turtle", prefixed, accept);
}
}
None => {
warn!("{} accept header missing, using RDF Turtle", prefixed);
}
}
debug!("{} RDF Turtle {:?}", prefixed, t.elapsed());
HttpResponse::Ok().content_type("application/turtle").body(rdf::serialize_turtle(&suffix))
}
}
}
#[get("/")]
async fn index() -> impl Responder {
match template().render("index", config()) {
Ok(body) => HttpResponse::Ok().content_type("text/html").body(body),
Err(e) => {
let message = format!("Could not render index page: {e:?}");
error!("{}", message);
HttpResponse::InternalServerError().body(message)
}
}
}
#[get("/about")]
async fn about_page() -> impl Responder {
match template().render("about", &About::new()) {
Ok(body) => HttpResponse::Ok().content_type("text/html").body(body),
Err(e) => {
let message = format!("Could not render about page: {e:?}");
error!("{}", message);
HttpResponse::InternalServerError().body(message)
}
}
}
#[get("")]
async fn redirect() -> impl Responder { HttpResponse::TemporaryRedirect().append_header(("location", config().base.clone() + "/")).finish() }
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let _ = config(); info!("RickView {} serving {} at http://localhost:{}{}/", config::VERSION, config().namespace, config().port, config().base);
HttpServer::new(move || {
App::new().service(css).service(favicon).service(scope(&config().base).service(index).service(about_page).service(redirect).service(res_html))
})
.bind(("0.0.0.0", config().port))?
.run()
.await
}