diff options
author | Adam T. Carpenter <atc@53hor.net> | 2024-11-14 21:49:47 -0500 |
---|---|---|
committer | Adam T. Carpenter <atc@53hor.net> | 2024-11-14 21:49:47 -0500 |
commit | 4aa45ef3e7798ee18bea8b49af75e383afce02a1 (patch) | |
tree | 7a1353753d9300929b43a561ff2f1aae59b6434f /src | |
parent | 014e34fa4a8cd4e3cdb3573a7748696c68873523 (diff) | |
parent | fc0e8296178ca779a270d91b681777f50b3b626d (diff) | |
download | carpentertutoring-master.tar.xz carpentertutoring-master.zip |
Diffstat (limited to 'src')
-rw-r--r-- | src/handlers.rs | 40 | ||||
-rw-r--r-- | src/helpers.rs | 5 | ||||
-rw-r--r-- | src/main.rs | 61 | ||||
-rw-r--r-- | src/posts.rs | 3 | ||||
-rw-r--r-- | src/posts/abstractions.rs | 2 | ||||
-rw-r--r-- | src/posts/abstractions/post.rs | 6 | ||||
-rw-r--r-- | src/posts/abstractions/repo.rs | 6 | ||||
-rw-r--r-- | src/posts/fs_post.rs | 47 | ||||
-rw-r--r-- | src/posts/fs_post_repo.rs | 32 | ||||
-rw-r--r-- | src/tutors.rs | 3 | ||||
-rw-r--r-- | src/tutors/abstractions.rs | 2 | ||||
-rw-r--r-- | src/tutors/abstractions/tutor.rs | 7 | ||||
-rw-r--r-- | src/tutors/abstractions/tutor_repo.rs | 5 | ||||
-rw-r--r-- | src/tutors/fs_tutor.rs | 48 | ||||
-rw-r--r-- | src/tutors/fs_tutor_repo.rs | 29 | ||||
-rw-r--r-- | src/views.rs | 6 | ||||
-rw-r--r-- | src/views/about.rs | 17 | ||||
-rw-r--r-- | src/views/brochure.rs | 6 | ||||
-rw-r--r-- | src/views/index.rs | 6 | ||||
-rw-r--r-- | src/views/policies.rs | 6 | ||||
-rw-r--r-- | src/views/post.rs | 15 | ||||
-rw-r--r-- | src/views/posts.rs | 18 |
22 files changed, 370 insertions, 0 deletions
diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..800d8f8 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,40 @@ +use askama::Template; +use crate::views::post::PostView; +use crate::views::posts::PostsView; +use crate::posts::abstractions::repo::PostRepo; +use crate::views::policies::PoliciesTemplate; +use crate::views::index::IndexTemplate; +use crate::views::brochure::BrochureTemplate; +use crate::views::about::AboutView; +use crate::tutors::abstractions::tutor_repo::TutorRepo; +use std::sync::Arc; +use axum::response::Html; +use axum::extract::{State, Path}; + +pub async fn about_handler(State(repo): State<Arc<impl TutorRepo>>) -> Html<String> { + let view = AboutView::with_tutors(repo.load()); + Html(view.render().unwrap()) +} + +pub async fn brochure_handler() -> Html<String> { + Html(BrochureTemplate{}.render().unwrap()) +} + +pub async fn index_handler() -> Html<String> { + Html(IndexTemplate {}.render().unwrap()) +} + +pub async fn policies_handler() -> Html<String> { + Html(PoliciesTemplate{}.render().unwrap()) +} + +pub async fn posts_handler(State(repo): State<Arc<impl PostRepo>>) -> Html<String> { + let view = PostsView::with_posts(repo.load()); + Html(view.render().unwrap()) +} + +pub async fn post_handler(Path(post_id): Path<String>, State(repo): State<Arc<impl PostRepo>>) -> Html<String> { + let view = PostView::with_post(repo.by_id(&post_id)); + Html(view.render().unwrap()) +} + diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..c7509de --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,5 @@ +use chrono::{prelude::*, Utc}; + +pub fn current_year() -> i32 { + Utc::now().year() +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..252588d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,61 @@ +use tower::Layer; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use tower_http::{trace::{self, TraceLayer}, normalize_path::NormalizePathLayer}; +use tracing::{info, Level}; +use axum::{routing::get, Router, ServiceExt, extract::Request}; +use tutors::fs_tutor_repo::FsTutorRepo; +use std::{sync::Arc, env}; +use tower_http::services::ServeDir; +use posts::fs_post_repo::FsPostRepo; + +mod helpers; +mod posts; +mod tutors; +mod views; +mod handlers; + +#[tokio::main] +async fn main() { + let tracing_filter = EnvFilter::builder() + .with_env_var("CT_LOG") + .try_from_env() + .unwrap_or("carpentertutoring=debug,tower_http=debug,axum::rejection=trace".into()); + tracing_subscriber::registry() + .with(tracing_filter) + .with(tracing_subscriber::fmt::layer()) + .init(); + + info!("loading state..."); + let blog_dir = env::var("CT_POSTS").unwrap(); + let tutor_dir = env::var("CT_TEAM").unwrap(); + let assets_dir = env::var("CT_ASSETS").unwrap(); + + let posts = Arc::new(FsPostRepo::with_dir(blog_dir)); + let tutors = Arc::new(FsTutorRepo::with_dir(tutor_dir.clone())); + + info!("initializing router..."); + let app = Router::new() + .route("/", get(handlers::index_handler)) + .route("/posts", get(handlers::posts_handler)) + .route("/posts/:post_id", get(handlers::post_handler)) + .with_state(posts) + .route("/policies", get(handlers::policies_handler)) + .route("/brochure", get(handlers::brochure_handler)) + .route("/about", get(handlers::about_handler)) + .with_state(tutors) + .nest_service("/assets", ServeDir::new(assets_dir)) + .nest_service("/team", ServeDir::new(tutor_dir)) + .fallback_service(ServeDir::new("static")) + .layer( + TraceLayer::new_for_http() + .make_span_with(trace::DefaultMakeSpan::new() + .level(Level::INFO)) + .on_response(trace::DefaultOnResponse::new() + .level(Level::INFO)) + ); + let app = NormalizePathLayer::trim_trailing_slash().layer(app); + + let addr = env::var("CT_BIND").unwrap_or("0.0.0.0:8000".into()); + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, ServiceExt::<Request>::into_make_service(app)).await.unwrap(); +} diff --git a/src/posts.rs b/src/posts.rs new file mode 100644 index 0000000..7f2c217 --- /dev/null +++ b/src/posts.rs @@ -0,0 +1,3 @@ +pub mod abstractions; +pub mod fs_post; +pub mod fs_post_repo; diff --git a/src/posts/abstractions.rs b/src/posts/abstractions.rs new file mode 100644 index 0000000..96c8ced --- /dev/null +++ b/src/posts/abstractions.rs @@ -0,0 +1,2 @@ +pub mod post; +pub mod repo; diff --git a/src/posts/abstractions/post.rs b/src/posts/abstractions/post.rs new file mode 100644 index 0000000..a941281 --- /dev/null +++ b/src/posts/abstractions/post.rs @@ -0,0 +1,6 @@ +use std::{borrow::Cow, fmt}; + +pub trait Post: fmt::Debug + Ord { + fn get_title(&self) -> &str; + fn get_article(&self) -> Cow<str>; +} diff --git a/src/posts/abstractions/repo.rs b/src/posts/abstractions/repo.rs new file mode 100644 index 0000000..6fd5d08 --- /dev/null +++ b/src/posts/abstractions/repo.rs @@ -0,0 +1,6 @@ +use crate::posts::abstractions::post::Post; + +pub trait PostRepo { + fn load(&self) -> impl IntoIterator<Item = impl Post>; + fn by_id(&self, post_id: &str) -> impl Post; +} diff --git a/src/posts/fs_post.rs b/src/posts/fs_post.rs new file mode 100644 index 0000000..8b8e725 --- /dev/null +++ b/src/posts/fs_post.rs @@ -0,0 +1,47 @@ +use crate::posts::abstractions::post::Post; +use std::{borrow::Cow, fs, path::PathBuf}; + +#[derive(Debug, Eq)] +pub struct FsPost { + file: PathBuf, +} + +impl FsPost { + pub fn with_path(path: PathBuf) -> Self { + Self { file: path } + } +} + +impl Post for FsPost { + fn get_title(&self) -> &str { + self.file.file_stem().unwrap().to_str().unwrap() + } + + fn get_article(&self) -> Cow<str> { + let article = fs::read_to_string(&self.file).unwrap(); + Cow::Owned(article) + } +} + +impl Ord for FsPost { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.file + .metadata() + .unwrap() + .modified() + .unwrap() + .cmp(&other.file.metadata().unwrap().modified().unwrap()) + } +} + +impl PartialOrd for FsPost { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl PartialEq for FsPost { + fn eq(&self, other: &Self) -> bool { + self.get_title() == other.get_title() + } +} diff --git a/src/posts/fs_post_repo.rs b/src/posts/fs_post_repo.rs new file mode 100644 index 0000000..13f797b --- /dev/null +++ b/src/posts/fs_post_repo.rs @@ -0,0 +1,32 @@ +use crate::posts::abstractions::post::Post; +use crate::posts::abstractions::repo::PostRepo; +use crate::posts::fs_post::FsPost; +use std::{fs, path::PathBuf}; + +pub struct FsPostRepo { + dir: PathBuf, +} + +impl FsPostRepo { + pub fn with_dir(path: impl Into<PathBuf>) -> Self { + Self { dir: path.into() } + } +} + +impl PostRepo for FsPostRepo { + fn load(&self) -> impl IntoIterator<Item = FsPost> { + let files = fs::read_dir(&self.dir).unwrap(); + files + .flatten() + .filter(|d| !d.file_name().to_string_lossy().starts_with('.')) + .map(|d| FsPost::with_path(d.path())) + } + + fn by_id(&self, post_id: &str) -> FsPost { + let posts = self.load(); + posts + .into_iter() + .find(|p| p.get_title() == post_id) + .unwrap() + } +} diff --git a/src/tutors.rs b/src/tutors.rs new file mode 100644 index 0000000..d90ef99 --- /dev/null +++ b/src/tutors.rs @@ -0,0 +1,3 @@ +pub mod abstractions; +pub mod fs_tutor; +pub mod fs_tutor_repo; diff --git a/src/tutors/abstractions.rs b/src/tutors/abstractions.rs new file mode 100644 index 0000000..a93c25d --- /dev/null +++ b/src/tutors/abstractions.rs @@ -0,0 +1,2 @@ +pub mod tutor; +pub mod tutor_repo; diff --git a/src/tutors/abstractions/tutor.rs b/src/tutors/abstractions/tutor.rs new file mode 100644 index 0000000..f36da3d --- /dev/null +++ b/src/tutors/abstractions/tutor.rs @@ -0,0 +1,7 @@ +use std::{borrow::Cow, fmt}; + +pub trait Tutor: fmt::Debug + Ord { + fn get_name(&self) -> &str; + fn get_id(&self) -> &str; + fn get_blurb(&self) -> Cow<str>; +} diff --git a/src/tutors/abstractions/tutor_repo.rs b/src/tutors/abstractions/tutor_repo.rs new file mode 100644 index 0000000..e017806 --- /dev/null +++ b/src/tutors/abstractions/tutor_repo.rs @@ -0,0 +1,5 @@ +use crate::tutors::abstractions::tutor::Tutor; + +pub trait TutorRepo { + fn load(&self) -> impl IntoIterator<Item = impl Tutor>; +} diff --git a/src/tutors/fs_tutor.rs b/src/tutors/fs_tutor.rs new file mode 100644 index 0000000..dc8a635 --- /dev/null +++ b/src/tutors/fs_tutor.rs @@ -0,0 +1,48 @@ +use crate::tutors::abstractions::tutor::Tutor; +use std::{borrow::Cow, cmp::Ordering, fs, path::PathBuf}; + +#[derive(Debug, Eq)] +pub struct FsTutor { + dir: PathBuf, +} + +impl FsTutor { + pub fn with_dir(path: PathBuf) -> Self { + Self { dir: path } + } +} + +impl Tutor for FsTutor { + fn get_id(&self) -> &str { + self.dir.file_name().unwrap().to_str().unwrap() + } + + fn get_name(&self) -> &str { + self.get_id() + } + + fn get_blurb(&self) -> Cow<str> { + let mut path = self.dir.to_owned(); + path.push(format!("{}.md", self.get_id())); + let blurb = fs::read_to_string(path).unwrap(); + Cow::Owned(blurb) + } +} + +impl Ord for FsTutor { + fn cmp(&self, other: &Self) -> Ordering { + self.get_id().cmp(other.get_id()) + } +} + +impl PartialOrd for FsTutor { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl PartialEq for FsTutor { + fn eq(&self, other: &Self) -> bool { + self.get_id() == other.get_id() + } +} diff --git a/src/tutors/fs_tutor_repo.rs b/src/tutors/fs_tutor_repo.rs new file mode 100644 index 0000000..9b9a8d9 --- /dev/null +++ b/src/tutors/fs_tutor_repo.rs @@ -0,0 +1,29 @@ +use crate::tutors::abstractions::tutor_repo::TutorRepo; +use crate::tutors::fs_tutor::FsTutor; +use std::{fs, path::PathBuf}; + +pub struct FsTutorRepo { + dir: PathBuf, +} + +impl FsTutorRepo { + pub fn with_dir(path: impl Into<PathBuf>) -> Self { + Self { dir: path.into() } + } +} + +impl TutorRepo for FsTutorRepo { + fn load(&self) -> impl IntoIterator<Item = FsTutor> { + let dirs = fs::read_dir(&self.dir).unwrap(); + dirs.flatten() + .filter(|d| { + !d.path() + .file_stem() + .unwrap() + .to_str() + .unwrap_or_default() + .starts_with('.') + }) + .map(|d| FsTutor::with_dir(d.path())) + } +} diff --git a/src/views.rs b/src/views.rs new file mode 100644 index 0000000..cb58813 --- /dev/null +++ b/src/views.rs @@ -0,0 +1,6 @@ +pub mod about; +pub mod brochure; +pub mod index; +pub mod policies; +pub mod post; +pub mod posts; diff --git a/src/views/about.rs b/src/views/about.rs new file mode 100644 index 0000000..349c9de --- /dev/null +++ b/src/views/about.rs @@ -0,0 +1,17 @@ +use crate::helpers::*; +use crate::tutors::abstractions::tutor::Tutor; +use askama::Template; + +#[derive(Template)] +#[template(path = "about/index.html")] +pub struct AboutView<T: Tutor> { + tutors: Vec<T>, +} + +impl<T: Tutor> AboutView<T> { + pub fn with_tutors(tutors: impl IntoIterator<Item = T>) -> Self { + let mut tutors: Vec<T> = tutors.into_iter().collect(); + tutors.sort(); + Self { tutors } + } +} diff --git a/src/views/brochure.rs b/src/views/brochure.rs new file mode 100644 index 0000000..0d2f8fc --- /dev/null +++ b/src/views/brochure.rs @@ -0,0 +1,6 @@ +use crate::helpers::*; +use askama::Template; + +#[derive(Template)] +#[template(path = "brochure/index.html")] +pub struct BrochureTemplate; diff --git a/src/views/index.rs b/src/views/index.rs new file mode 100644 index 0000000..3ced24d --- /dev/null +++ b/src/views/index.rs @@ -0,0 +1,6 @@ +use crate::helpers::*; +use askama::Template; + +#[derive(Template)] +#[template(path = "index.html")] +pub struct IndexTemplate; diff --git a/src/views/policies.rs b/src/views/policies.rs new file mode 100644 index 0000000..3d9787d --- /dev/null +++ b/src/views/policies.rs @@ -0,0 +1,6 @@ +use crate::helpers::*; +use askama::Template; + +#[derive(Template)] +#[template(path = "policies.html")] +pub struct PoliciesTemplate; diff --git a/src/views/post.rs b/src/views/post.rs new file mode 100644 index 0000000..4f0554b --- /dev/null +++ b/src/views/post.rs @@ -0,0 +1,15 @@ +use crate::helpers::*; +use crate::posts::abstractions::post::Post; +use askama::Template; + +#[derive(Template)] +#[template(path = "post.html")] +pub struct PostView<P: Post> { + post: P, +} + +impl<P: Post> PostView<P> { + pub fn with_post(post: P) -> Self { + Self { post } + } +} diff --git a/src/views/posts.rs b/src/views/posts.rs new file mode 100644 index 0000000..82b5996 --- /dev/null +++ b/src/views/posts.rs @@ -0,0 +1,18 @@ +use crate::helpers::*; +use crate::posts::abstractions::post::Post; +use askama::Template; + +#[derive(Template)] +#[template(path = "posts.html")] +pub struct PostsView<P: Post> { + posts: Vec<P>, +} + +impl<P: Post> PostsView<P> { + pub fn with_posts(posts: impl IntoIterator<Item = P>) -> Self { + let mut posts: Vec<P> = posts.into_iter().collect(); + posts.sort(); + posts.reverse(); + Self { posts } + } +} |