A => .gitignore +1 -0
A => Cargo.toml +55 -0
@@ 1,55 @@
+[package]
+name = "pull-list-gen"
+version = "0.1.0"
+edition = "2021"
+
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = [ "env-filter" ] }
+
+tower = { version = "0.4", features = [ "util", "timeout" ] }
+tower-http = { version = "0.3", features = [ "add-extension", "trace" ] }
+
+color-eyre = "0.6"
+eyre = "0.6"
+
+thiserror = "1"
+
+kankyo = "0.3"
+
+axum-macros = "0.2"
+
+paseto = "2"
+ring = "0.16"
+
+rand = "0.8"
+
+chrono = "0.4"
+
+tectonic = "0.14"
+
+tera = "1"
+
+[dependencies.serde]
+version = "1"
+features = [
+ "derive"
+]
+
+[dependencies.axum]
+version = "0.5"
+features = [
+ "json",
+ "tower-log"
+]
+
+[dependencies.hyper]
+version = "0.14"
+features = [ "full" ]
+
+[dependencies.tokio]
+version = "1"
+features = [ "full" ]
A => src/build.rs +7 -0
@@ 1,7 @@
+use ructe::{Ructe, RucteError};
+use std::process::Command;
+
+fn main() -> ructe::Result<()> {
+ Ructe::from_env()?.compile_templates("templates")
+}
+
A => src/errors.rs +38 -0
@@ 1,38 @@
+use axum::{response::{IntoResponse, Response}, body};
+use axum::http::StatusCode;
+use thiserror::Error;
+
+
+#[derive(Debug, Error)]
+pub enum ServiceError {
+ #[error("Axum error: {0}")]
+ Axum(#[from] axum::Error),
+
+ #[error("Not Found")]
+ NotFound,
+
+ #[error("Generic: {0}")]
+ Generic(String),
+
+ #[error("LaTeX: {0}")]
+ Tectonic(#[from] tectonic::Error),
+}
+
+pub type StringResult<T = &'static str> = std::result::Result<T, ServiceError>;
+
+pub type JsonResult<T> = std::result::Result<T, ServiceError>;
+
+pub type NoneResult = std::result::Result<(), ServiceError>;
+
+impl IntoResponse for ServiceError {
+ fn into_response(self) -> Response {
+ let body = body::boxed(body::Full::from(self.to_string()));
+
+ let status = match self {
+ ServiceError::NotFound => StatusCode::NOT_FOUND,
+ _ => StatusCode::INTERNAL_SERVER_ERROR,
+ };
+
+ Response::builder().status(status).body(body).unwrap()
+ }
+}
A => src/handlers.rs +34 -0
@@ 1,34 @@
+use axum::Json;
+
+use crate::{models::{PullTicket, Orientation}, errors::{JsonResult, StringResult}};
+
+
+
+
+
+pub async fn render_ticket(Json(ticket): Json<PullTicket>) -> StringResult {
+ let mut buf: Vec<u8> = vec![];
+ match ticket.orientation {
+ Orientation::Portrait => {
+ crate::templates::
+ crate::templates::portrait_html(
+ &mut buf,
+ ticket.event_name,
+ ticket.required_date,
+ ticket.crew_chief,
+ ticket.fourth_line,
+ ticket.equipment)?;
+ },
+ Orientation::Landscape => {
+ crate::templates::landscape_html(
+ &mut buf,
+ ticket.event_name,
+ ticket.required_date,
+ ticket.crew_chief,
+ ticket.fourth_line,
+ ticket.equipment)?;
+ }
+ }
+
+ Ok("OK")
+}
A => src/main.rs +61 -0
@@ 1,61 @@
+use std::{time::Duration, net::SocketAddr, str::FromStr};
+
+use axum::{Router, error_handling::HandleErrorLayer, BoxError, routing::get, response::IntoResponse, handler::Handler};
+use hyper::StatusCode;
+use tower::ServiceBuilder;
+use tower_http::trace::TraceLayer;
+use tracing_subscriber::prelude::*;
+use tracing::{debug, info, warn};
+
+mod errors;
+mod models;
+mod handlers;
+
+#[tokio::main]
+async fn main() {
+ if let Err(e) = kankyo::init() {
+ warn!("No .env found, using defaults! ({})", e);
+ }
+ color_eyre::install().unwrap();
+
+ tracing_subscriber::registry()
+ .with(tracing_subscriber::EnvFilter::new(std::env::var("RUST_LOG").unwrap_or_else(|_| "debug".into()),
+ ))
+ .with(tracing_subscriber::fmt::layer())
+ .init();
+
+ debug!("Beginning initialization");
+
+ let app = Router::new()
+ .route("/health", get(health_check))
+ .layer(
+ ServiceBuilder::new()
+ .layer(HandleErrorLayer::new(|error: BoxError| async move {
+ if error.is::<tower::timeout::error::Elapsed>() {
+ Ok(StatusCode::REQUEST_TIMEOUT)
+ } else {
+ Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Unhandled internal error: {}", error)))
+ }
+ })
+ )
+ .timeout(Duration::from_secs(10))
+ .layer(TraceLayer::new_for_http())
+ .into_inner(),
+ )
+ .fallback(handler_404.into_service());
+
+ let addr = SocketAddr::from_str(std::env::var("BIND_ADDR").unwrap_or_else(|_| "127.0.0.1:8000".into()).as_str()).unwrap();
+ info!("Listening on {}", addr);
+
+ axum::Server::bind(&addr)
+ .serve(app.into_make_service())
+ .await.unwrap();
+}
+
+async fn health_check() ->&'static str {
+ "OK"
+}
+
+async fn handler_404() -> impl IntoResponse {
+ StatusCode::NOT_FOUND
+}
A => src/models.rs +39 -0
@@ 1,39 @@
+use serde::Deserialize;
+
+use crate::errors::ServiceError;
+
+#[derive(Deserialize, Debug)]
+pub enum Orientation {
+ Portrait,
+ Landscape
+}
+
+impl TryFrom<&str> for Orientation {
+ type Error = ServiceError;
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
+ let portrait = String::from("portrait");
+ let landscape = String::from("landscape");
+ match value.to_lowercase() {
+ portrait => Ok(Orientation::Portrait),
+ landscape => Ok(Orientation::Landscape),
+ _ => Err(ServiceError::Generic(String::from("Invalid orientation")))
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct PullTicket {
+ pub event_name: String,
+ pub required_date: String,
+ pub crew_chief: String,
+ pub orientation: Orientation,
+ pub fourth_line: Option<String>,
+ pub equipment: Vec<LineItem>
+}
+
+#[derive(Debug, Deserialize)]
+pub struct LineItem {
+ pub name: String,
+ pub quantity: u32,
+ pub destination: String
+}
A => templates/landscape.rs.html +56 -0
@@ 1,56 @@
+@use crate::models::LineItem;
+@(event_name: &str, required_date: &str, crew_chief: &str, 4th_line:
+Option<&str>, equipment: Vec<LineItem>)
+
+\documentclass[landscape]{article}
+\usepackage{graphicx} % Required for inserting images
+\usepackage[margin=0.5in]{geometry}
+\renewcommand{\familydefault}{\sfdefault}
+\usepackage{anyfontsize}
+\title{Pull Ticket Template}
+\author{csalter2 }
+\date{January 2024}
+\usepackage{setspace}
+
+\usepackage{nopageno}
+
+\begin{document}
+
+\begin{center}
+
+\begin{minipage}[c][2in]{0.4\textwidth}
+ \centering
+ \includegraphics[scale=0.1]{LNL Stencil.png}
+ \label{fig:enter-label}
+ \end{minipage}
+ \begin{minipage}[c][2in]{0.4\textwidth}
+ \begin{spacing}{2.5}
+ {\fontsize{55}{60}\selectfont @event_name}\\
+ {\fontsize{25}{27}\selectfont Required On: @required_date}\\
+ {\fontsize{25}{27}\selectfont Crew Chief: @crew_chief}\\
+ @if 4th_line.is_some() {
+ {\fontsize{25}{27}\selectfont @4th_line}
+ }
+ \end{spacing}
+ \end{minipage}
+\vspace{5mm}
+\hrule
+\vspace{7mm}
+\begin{Huge}
+\bgroup
+\def\arraystretch{1.3}
+\begin{tabular}{|c|c|c|}
+ \hline
+ \textbf{Equipment} & \rule{1em}{0pt}\textbf{Quantity}\rule{1em}{0pt} & \rule{1em}{0pt}\textbf{Destination}\rule{1em}{0pt} \\
+ \hline
+ \hline
+ @for item in equipment {
+ @item.name & @item.quantity & @item.destination \\
+ \hline
+ }
+\end{tabular}
+\egroup
+\end{Huge}
+\end{center}
+\end{document}
+
A => templates/portrait.rs.html +55 -0
@@ 1,55 @@
+@use crate::models::LineItem;
+@(event_name: &str, required_date: &str, crew_chief: &str, fourth_line:
+Option<&str>, equipment: Vec<LineItem>)
+\documentclass{article}
+\usepackage{graphicx} % Required for inserting images
+\usepackage[margin=0.5in]{geometry}
+\renewcommand{\familydefault}{\sfdefault}
+\usepackage{anyfontsize}
+\title{Pull Ticket Template}
+\author{csalter2 }
+\date{January 2024}
+\usepackage{setspace}
+
+\usepackage{nopageno}
+
+\begin{document}
+
+\begin{center}
+
+\begin{minipage}[c][2in]{0.4\textwidth}
+ \centering
+ \includegraphics[scale=0.1]{LNL Stencil.png}
+ \label{fig:enter-label}
+ \end{minipage}
+ \begin{minipage}[c][2in]{0.4\textwidth}
+ \begin{spacing}{2.5}
+ {\fontsize{50}{57}\selectfont @event_name}\\
+ {\fontsize{20}{20}\selectfont Required On: @required_date}\\
+ {\fontsize{20}{20}\selectfont Crew Chief: @crew_chief}\\
+ @if 4th_line.is_some() {
+ {\fontsize{20}{20}\selectfont @4th_line}
+ }
+ \end{spacing}
+ \end{minipage}
+
+\vspace{5mm}
+\hrule
+\vspace{7mm}
+\begin{huge}
+\bgroup
+\def\arraystretch{1.3}
+\begin{tabular}{|c|c|c|}
+ \hline
+ \textbf{Equipment} & \rule{1em}{0pt}\textbf{Quantity}\rule{1em}{0pt} & \rule{1em}{0pt}\textbf{Destination}\rule{1em}{0pt} \\
+ \hline
+ @for item in equipment {
+ @item.name & @item.quantity & @item.destination \\
+ \hline
+ }
+ \end{tabular}
+\egroup
+\end{huge}
+\end{center}
+\end{document}
+