summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAdam T. Carpenter <atc@53hor.net>2024-11-14 21:49:47 -0500
committerAdam T. Carpenter <atc@53hor.net>2024-11-14 21:49:47 -0500
commit4aa45ef3e7798ee18bea8b49af75e383afce02a1 (patch)
tree7a1353753d9300929b43a561ff2f1aae59b6434f /src
parent014e34fa4a8cd4e3cdb3573a7748696c68873523 (diff)
parentfc0e8296178ca779a270d91b681777f50b3b626d (diff)
downloadcarpentertutoring-master.tar.xz
carpentertutoring-master.zip
Merge branch 'release'HEADmaster
Diffstat (limited to 'src')
-rw-r--r--src/handlers.rs40
-rw-r--r--src/helpers.rs5
-rw-r--r--src/main.rs61
-rw-r--r--src/posts.rs3
-rw-r--r--src/posts/abstractions.rs2
-rw-r--r--src/posts/abstractions/post.rs6
-rw-r--r--src/posts/abstractions/repo.rs6
-rw-r--r--src/posts/fs_post.rs47
-rw-r--r--src/posts/fs_post_repo.rs32
-rw-r--r--src/tutors.rs3
-rw-r--r--src/tutors/abstractions.rs2
-rw-r--r--src/tutors/abstractions/tutor.rs7
-rw-r--r--src/tutors/abstractions/tutor_repo.rs5
-rw-r--r--src/tutors/fs_tutor.rs48
-rw-r--r--src/tutors/fs_tutor_repo.rs29
-rw-r--r--src/views.rs6
-rw-r--r--src/views/about.rs17
-rw-r--r--src/views/brochure.rs6
-rw-r--r--src/views/index.rs6
-rw-r--r--src/views/policies.rs6
-rw-r--r--src/views/post.rs15
-rw-r--r--src/views/posts.rs18
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 }
+ }
+}