feat: load templates

This commit is contained in:
Aravinth Manivannan 2022-05-18 20:28:26 +05:30
parent 5877c39bef
commit 6cd0313a01
Signed by: realaravinth
GPG key ID: AD9F0F08E855ED88
5 changed files with 399 additions and 9 deletions

View file

@ -18,13 +18,17 @@
use std::sync::Arc;
use actix_web::{middleware, web::Data, App, HttpServer};
use lazy_static::lazy_static;
pub mod ctx;
pub mod db;
pub mod errors;
pub mod federate;
pub mod forge;
pub mod pages;
pub mod settings;
pub mod spider;
pub mod static_assets;
#[cfg(test)]
mod tests;
pub mod utils;
@ -34,22 +38,30 @@ use crate::federate::{get_federate, ArcFederate};
use ctx::Ctx;
use db::{sqlite, BoxDB};
use settings::Settings;
use static_assets::FileMap;
pub use crate::pages::routes::PAGES;
pub const CACHE_AGE: u32 = 60 * 60 * 24 * 30; // one month, I think?
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const PKG_NAME: &str = env!("CARGO_PKG_NAME");
pub const GIT_COMMIT_HASH: &str = env!("GIT_HASH");
pub const DOMAIN: &str = "developer-starchart.forgeflux.org";
pub type ArcCtx = Arc<Ctx>;
pub type WebCtx = Data<ArcCtx>;
pub type WebData = Data<BoxDB>;
pub type WebFederate = Data<ArcFederate>;
lazy_static! {
pub static ref FILES: FileMap = FileMap::new();
}
#[actix_rt::main]
async fn main() {
let settings = Settings::new().unwrap();
pretty_env_logger::init();
lazy_static::initialize(&pages::TEMPLATES);
let ctx = Ctx::new(settings.clone()).await;
let db = sqlite::get_data(Some(settings.clone())).await;

106
src/pages/errors.rs Normal file
View file

@ -0,0 +1,106 @@
/*
* ForgeFlux StarChart - A federated software forge spider
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::fmt;
use actix_web::{
error::ResponseError,
http::{header::ContentType, StatusCode},
HttpResponse, HttpResponseBuilder,
};
use derive_more::Display;
use derive_more::Error;
use serde::*;
use super::TemplateFile;
use crate::errors::ServiceError;
pub const ERROR_KEY: &str = "error";
pub const ERROR_TEMPLATE: TemplateFile = TemplateFile::new("error_comp", "components/error.html");
pub fn register_templates(t: &mut tera::Tera) {
ERROR_TEMPLATE.register(t).expect(ERROR_TEMPLATE.name);
}
/// Render template with error context
pub trait CtxError {
fn with_error(&self, e: &ReadableError) -> String;
}
#[derive(Serialize, Debug, Display, Clone)]
#[display(fmt = "title: {} reason: {}", title, reason)]
pub struct ReadableError {
pub reason: String,
pub title: String,
}
impl ReadableError {
pub fn new(e: &ServiceError) -> Self {
let reason = format!("{}", e);
let title = format!("{}", e.status_code());
Self { reason, title }
}
}
#[derive(Error, Display)]
#[display(fmt = "{}", readable)]
pub struct PageError<T> {
#[error(not(source))]
template: T,
readable: ReadableError,
#[error(not(source))]
error: ServiceError,
}
impl<T> fmt::Debug for PageError<T> {
#[cfg(not(tarpaulin_include))]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PageError")
.field("readable", &self.readable)
.finish()
}
}
impl<T: CtxError> PageError<T> {
/// create new instance of [PageError] from a template and an error
pub fn new(template: T, error: ServiceError) -> Self {
let readable = ReadableError::new(&error);
Self {
error,
template,
readable,
}
}
}
#[cfg(not(tarpaulin_include))]
impl<T: CtxError> ResponseError for PageError<T> {
fn error_response(&self) -> HttpResponse {
HttpResponseBuilder::new(self.status_code())
.content_type(ContentType::html())
.body(self.template.with_error(&self.readable))
}
fn status_code(&self) -> StatusCode {
self.error.status_code()
}
}
/// Generic result data structure
#[cfg(not(tarpaulin_include))]
pub type PageResult<V, T> = std::result::Result<V, PageError<T>>;

175
src/pages/mod.rs Normal file
View file

@ -0,0 +1,175 @@
/*
* ForgeFlux StarChart - A federated software forge spider
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::*;
use lazy_static::lazy_static;
use rust_embed::RustEmbed;
use serde::*;
use tera::*;
use crate::settings::Settings;
use crate::static_assets::ASSETS;
use crate::PAGES;
use crate::{GIT_COMMIT_HASH, VERSION};
mod errors;
pub mod routes;
pub struct TemplateFile {
pub name: &'static str,
pub path: &'static str,
}
impl TemplateFile {
pub const fn new(name: &'static str, path: &'static str) -> Self {
Self { name, path }
}
pub fn register(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
t.add_raw_template(self.name, &Templates::get_template(self).expect(self.name))
}
#[cfg(test)]
#[allow(dead_code)]
pub fn register_from_file(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
use std::path::Path;
t.add_template_file(Path::new("templates/").join(self.path), Some(self.name))
}
}
pub const PAYLOAD_KEY: &str = "payload";
pub const BASE: TemplateFile = TemplateFile::new("base", "components/base.html");
pub const FOOTER: TemplateFile = TemplateFile::new("footer", "components/footer.html");
pub const PUB_NAV: TemplateFile = TemplateFile::new("pub_nav", "components/nav/pub.html");
pub const AUTH_NAV: TemplateFile = TemplateFile::new("auth_nav", "components/nav/auth.html");
lazy_static! {
pub static ref TEMPLATES: Tera = {
let mut tera = Tera::default();
for t in [BASE, FOOTER, PUB_NAV, AUTH_NAV].iter() {
t.register(&mut tera).unwrap();
}
errors::register_templates(&mut tera);
tera.autoescape_on(vec![".html", ".sql"]);
//auth::register_templates(&mut tera);
//gists::register_templates(&mut tera);
tera
};
}
#[derive(RustEmbed)]
#[folder = "templates/"]
pub struct Templates;
impl Templates {
pub fn get_template(t: &TemplateFile) -> Option<String> {
match Self::get(t.path) {
Some(file) => Some(String::from_utf8_lossy(&file.data).into_owned()),
None => None,
}
}
}
pub fn ctx(s: &Settings) -> Context {
let mut ctx = Context::new();
let footer = Footer::new(s);
ctx.insert("footer", &footer);
ctx.insert("page", &PAGES);
ctx.insert("assets", &*ASSETS);
ctx
}
#[derive(Serialize)]
pub struct Footer<'a> {
version: &'a str,
admin_email: &'a str,
source_code: &'a str,
git_hash: &'a str,
settings: &'a Settings,
}
impl<'a> Footer<'a> {
pub fn new(settings: &'a Settings) -> Self {
Self {
version: VERSION,
source_code: &settings.source_code,
admin_email: &settings.admin_email,
git_hash: &GIT_COMMIT_HASH[..8],
settings,
}
}
}
pub fn services(cfg: &mut web::ServiceConfig) {}
#[cfg(test)]
mod tests {
#[test]
fn templates_work_basic() {
use super::*;
use tera::Tera;
let mut tera = Tera::default();
let mut tera2 = Tera::default();
for t in [
BASE, FOOTER, PUB_NAV,
AUTH_NAV,
// auth::AUTH_BASE,
// auth::login::LOGIN,
// auth::register::REGISTER,
// errors::ERROR_TEMPLATE,
// gists::GIST_BASE,
// gists::GIST_EXPLORE,
// gists::new::NEW_GIST,
]
.iter()
{
t.register_from_file(&mut tera2).unwrap();
t.register(&mut tera).unwrap();
}
}
}
#[cfg(test)]
mod http_page_tests {
use actix_web::http::StatusCode;
use actix_web::test;
use crate::ctx::Ctx;
use crate::db::BoxDB;
use crate::tests::*;
use crate::*;
use super::PAGES;
#[actix_rt::test]
async fn sqlite_templates_work() {
let (db, data) = sqlx_sqlite::get_ctx().await;
templates_work(data, db).await;
}
async fn templates_work(data: Arc<Data>, db: BoxDB) {
let app = get_app!(data, db).await;
for file in [PAGES.auth.login, PAGES.auth.register].iter() {
let resp = get_request!(&app, file);
assert_eq!(resp.status(), StatusCode::OK);
}
}
}

97
src/pages/routes.rs Normal file
View file

@ -0,0 +1,97 @@
/*
* ForgeFlux StarChart - A federated software forge spider
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use serde::Serialize;
/// constant [Pages](Pages) instance
pub const PAGES: Pages = Pages::new();
#[derive(Serialize)]
/// Top-level routes data structure for V1 AP1
pub struct Pages {
/// home page
pub home: &'static str,
}
impl Pages {
/// create new instance of Routes
const fn new() -> Pages {
let home = "/";
Pages { home }
}
}
#[derive(Serialize)]
/// Authentication routes
pub struct Auth {
/// logout route
pub logout: &'static str,
/// login route
pub login: &'static str,
}
impl Auth {
/// create new instance of Authentication route
pub const fn new() -> Auth {
let login = "/login";
let logout = "/logout";
Auth { login, logout }
}
}
//#[cfg(test)]
//mod tests {
// use super::*;
// #[test]
// fn gist_route_substitution_works() {
// const NAME: &str = "bob";
// const GIST: &str = "foo";
// const FILE: &str = "README.md";
// let get_profile = format!("/~{NAME}");
// let view_gist = format!("/~{NAME}/{GIST}");
// let post_comment = format!("/~{NAME}/{GIST}/comment");
// let get_file = format!("/~{NAME}/{GIST}/contents/{FILE}");
//
// let profile_component = GistProfilePathComponent { username: NAME };
//
// assert_eq!(get_profile, PAGES.gist.get_profile_route(profile_component));
//
// let profile_component = PostCommentPath {
// username: NAME.into(),
// gist: GIST.into(),
// };
//
// assert_eq!(view_gist, PAGES.gist.get_gist_route(&profile_component));
//
// let post_comment_path = PostCommentPath {
// gist: GIST.into(),
// username: NAME.into(),
// };
//
// assert_eq!(
// post_comment,
// PAGES.gist.get_post_comment_route(&post_comment_path)
// );
//
// let file_component = GetFilePath {
// username: NAME.into(),
// gist: GIST.into(),
// file: FILE.into(),
// };
// assert_eq!(get_file, PAGES.gist.get_file_route(&file_component));
// }
//}

View file

@ -21,11 +21,11 @@ use std::{env, fs};
use config::{Config, ConfigError, Environment, File};
use derive_more::Display;
use log::warn;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use url::Url;
use validator::Validate;
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Server {
pub port: u32,
pub domain: String,
@ -40,7 +40,7 @@ impl Server {
}
}
#[derive(Deserialize, Display, Clone, Debug)]
#[derive(Debug, Display, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
#[display(fmt = "debug")]
@ -64,7 +64,7 @@ impl LogLevel {
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
pub struct Repository {
pub root: String,
}
@ -83,7 +83,7 @@ impl Repository {
}
}
#[derive(Deserialize, Display, PartialEq, Clone, Debug)]
#[derive(Debug, Display, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DBType {
#[display(fmt = "postgres")]
@ -102,14 +102,14 @@ impl DBType {
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Database {
pub url: String,
pub pool: u32,
pub database_type: DBType,
}
#[derive(Debug, Validate, Clone, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Crawler {
pub ttl: u64,
pub client_timeout: u64,
@ -117,7 +117,7 @@ pub struct Crawler {
pub wait_before_next_api_call: u64,
}
#[derive(Debug, Validate, Clone, Deserialize)]
#[derive(Debug, Validate, Clone, PartialEq, Serialize, Deserialize)]
pub struct Settings {
pub log: LogLevel,
pub database: Database,