~muirrum/pull-ticket-api

5db477d314842721ba3126ad1c9775c8d703bea8 — Cara Salter 10 months ago
WIP: Initial Commit
A  => .gitignore +1 -0
@@ 1,1 @@
/target

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}