use axum::{ extract::State, http::StatusCode, routing::{get, post, put, delete}, Json, Router, }; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::net::SocketAddr; use std::sync::Arc; use tower_http::cors::{Any, CorsLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[derive(Clone)] pub struct AppState { pub pool: PgPool, } #[derive(Debug, Serialize, Deserialize)] pub struct Job { pub id: uuid::Uuid, pub title: String, pub description: String, pub location: String, pub job_type: String, pub status: String, pub created_at: chrono::DateTime, } #[derive(Debug, Deserialize)] pub struct CreateJob { pub title: String, pub description: String, pub location: String, pub job_type: String, } async fn list_jobs(State(state): State>) -> Result>, StatusCode> { let jobs = sqlx::query_as::<_, Job>( "SELECT id, title, description, location, job_type, status, created_at FROM jobs ORDER BY created_at DESC" ) .fetch_all(&state.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(jobs)) } async fn create_job( State(state): State>, Json(payload): Json, ) -> Result, StatusCode> { let job = sqlx::query_as::<_, Job>( r#" INSERT INTO jobs (title, description, location, job_type) VALUES ($1, $2, $3, $4) RETURNING id, title, description, location, job_type, status, created_at "#, ) .bind(&payload.title) .bind(&payload.description) .bind(&payload.location) .bind(&payload.job_type) .fetch_one(&state.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(job)) } async fn get_job( State(state): State>, axum::extract::Path(id): axum::extract::Path, ) -> Result, StatusCode> { let job = sqlx::query_as::<_, Job>( "SELECT id, title, description, location, job_type, status, created_at FROM jobs WHERE id = $1" ) .bind(id) .fetch_optional(&state.pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::NOT_FOUND)?; Ok(Json(job)) } async fn health() -> &'static str { "Jobs Service OK" } #[tokio::main] async fn main() { tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), )) .with(tracing_subscriber::fmt::layer()) .init(); let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); let pool = sqlx::postgres::PgPoolOptions::new() .max_connections(10) .connect(&database_url) .await .expect("Failed to connect to database"); tracing::info!("Connected to database"); let state = Arc::new(AppState { pool }); let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); let app = Router::new() .route("/health", get(health)) .route("/jobs", get(list_jobs)) .route("/jobs", post(create_job)) .route("/jobs/:id", get(get_job)) .layer(cors) .with_state(state); let port: u16 = std::env::var("PORT") .unwrap_or_else(|_| "9103".to_string()) .parse() .expect("PORT must be a valid u16"); let addr = SocketAddr::from(([0, 0, 0, 0], port)); tracing::info!("Jobs service listening on {}", addr); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); }