From a474164964798b648c24da757e437ac1626068b0 Mon Sep 17 00:00:00 2001 From: Michal Vanko Date: Tue, 9 Jan 2024 19:54:25 +0100 Subject: [PATCH] parsing metadata from articles --- axum_server/.gitignore | 14 +++++ axum_server/Cargo.toml | 18 ++++++ axum_server/src/main.rs | 135 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 axum_server/.gitignore create mode 100644 axum_server/Cargo.toml create mode 100644 axum_server/src/main.rs diff --git a/axum_server/.gitignore b/axum_server/.gitignore new file mode 100644 index 0000000..6985cf1 --- /dev/null +++ b/axum_server/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/axum_server/Cargo.toml b/axum_server/Cargo.toml new file mode 100644 index 0000000..c1d0d56 --- /dev/null +++ b/axum_server/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "axum_server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = "0.7.3" +chrono = { version = "0.4.31", features = ["serde"] } +gray_matter = "0.2.6" +markdown = "1.0.0-alpha.16" +serde = "1.0.195" +serde_json = "1.0.111" +tokio = { version = "1.35.1", features = ["full"] } +tower-http = { version = "0.5.0", features = ["trace"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/axum_server/src/main.rs b/axum_server/src/main.rs new file mode 100644 index 0000000..47151e1 --- /dev/null +++ b/axum_server/src/main.rs @@ -0,0 +1,135 @@ +use axum::{ + extract::{MatchedPath, Path}, + http::{Request, StatusCode}, + response::Html, + routing::get, + Router, +}; +use chrono::{DateTime, Utc}; +use gray_matter::{engine::YAML, Matter}; +use markdown::{to_html_with_options, CompileOptions, Constructs, Options, ParseOptions}; +use serde::{Deserialize, Deserializer}; +use tokio::fs; +use tower_http::trace::TraceLayer; +use tracing::info_span; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[tokio::main] +async fn main() { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { + // axum logs rejections from built-in extractors with the `axum::rejection` + // target, at `TRACE` level. `axum::rejection=trace` enables showing those events + "axum_server=debug,tower_http=debug,axum::rejection=trace".into() + }), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + // build our application with a single route + let app = Router::new() + .route("/", get(|| async { "Hello, World!" })) + .route("/blog/:post_id", get(parse_post)) + .layer( + TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { + // Log the matched route's path (with placeholders not filled in). + // Use request.uri() or OriginalUri if you want the real path. + let matched_path = request + .extensions() + .get::() + .map(MatchedPath::as_str); + + info_span!( + "http_request", + method = ?request.method(), + matched_path, + some_other_field = tracing::field::Empty, + ) + }), + ); + + // run our app with hyper, listening globally on port 3080 + let listener = tokio::net::TcpListener::bind("0.0.0.0:3080").await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} + +async fn parse_post(Path(post_id): Path) -> Result, StatusCode> { + let path = format!("../_posts/blog/{}", post_id); + let contents = fs::read_to_string(path).await; + + let raw_content = match contents { + Err(_reason) => { + // TODO find the real reason + return Err(StatusCode::NOT_FOUND); + } + Ok(content) => content, + }; + + let markdown_options = Options { + parse: ParseOptions { + constructs: Constructs { + frontmatter: true, + ..Default::default() + }, + ..Default::default() + }, + compile: CompileOptions { + allow_dangerous_html: true, + ..Default::default() + }, + ..Default::default() + }; + + #[derive(Deserialize, Debug)] + struct Metadata { + layout: String, + title: String, + segments: Vec, + published: bool, + #[serde(deserialize_with = "deserialize_date")] + date: DateTime, + thumbnail: String, + tags: Vec, + } + + let matter = Matter::::new(); + let metadata = matter.parse_with_struct::(&raw_content); + + // Deserialize JSON into MyData struct + + // Print the entire struct using the Debug trait + println!("{:?}", metadata.unwrap().data); + + let parsed = to_html_with_options(&raw_content, &markdown_options); + + let content = match parsed { + Err(reason) => { + tracing::error!(reason); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + Ok(content) => content, + }; + + // TODO Parse file + return Ok(Html(content)); +} + +fn deserialize_date<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let date_str = String::deserialize(deserializer)?; + match DateTime::parse_from_rfc3339(&date_str) { + Ok(datetime) => Ok(datetime.with_timezone(&Utc)), + Err(err) => Err(serde::de::Error::custom(format!( + "Error parsing date: {}", + err + ))), + } +} + +// TODO Port from env variable +// TODO templating system +// TODO simple Logging +// TODO parse md files