use arc_swap::ArcSwap; use axum::extract::{Request, State}; use axum::http::StatusCode; use axum::routing::any; use axum::{Json, Router}; use bb8_postgres::PostgresConnectionManager; use clap::Parser; use log::{error, info}; use std::collections::HashMap; use std::fs; use std::sync::{Arc, LazyLock, OnceLock}; use tipi_lang::compiler::assembly_pass::AsmChunk; use tipi_lang::compiler::{compile_sourcedir, map_underlying, run}; use tipi_lang::errors::TipiLangError; use tipi_lang::value::Value; use tipi_lang::vm::run_async; use tipi_lang::{DB_POOL, vm}; use tokio_postgres::NoTls; /// A simple CLI tool to greet users #[derive(Parser, Debug)] struct Args { #[arg(short, long)] repl: bool, #[arg(short, long)] source: Option, #[arg(short, long)] watch: bool, #[arg(short, long)] file: Option, } #[tokio::main] async fn main() -> Result<(), TipiLangError> { //TODO make configurable let manager = PostgresConnectionManager::new_from_stringlike( "host=localhost user=postgres password=boompje", NoTls, ) .unwrap(); let pool = bb8::Pool::builder() .build(manager) .await .expect("cannot create the the database connection pool"); let _ = DB_POOL.set(pool); tracing_subscriber::fmt::init(); let args = Args::parse(); if let Some(file) = args.file { let source = fs::read_to_string(file).expect("Unable to read file"); run(&source)?; } else { info!("-- Tipilang --"); let source = args.source.unwrap_or("./source".to_string()); let registry = compile_sourcedir(&source)?; let empty = registry.is_empty(); let swap = Arc::new(ArcSwap::from(Arc::new(registry))); if !empty { let registry = swap.clone().load(); if let Some(main) = registry.get("/main") { vm::run(registry, "/main")?; } if args.watch { tipi_lang::file_watch::start_watch_daemon(&source, swap.clone()); } info!("-- Compilation successful --"); let state = AppState { registry: swap.clone(), }; let app = Router::new() .route("/", any(handle_any).with_state(state.clone())) .route("/{*path}", any(handle_any).with_state(state.clone())); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await .map_err(map_underlying())?; info!( "-- Listening on {} --\n", listener.local_addr().map_err(map_underlying())? ); if args.repl { let _ = std::thread::spawn(move || tipi_lang::repl::start(swap.clone())); } axum::serve(listener, app).await.map_err(map_underlying())?; } else { error!("No source files found or compilation error"); if args.repl { tipi_lang::repl::start(swap.clone())?; } } } Ok(()) } #[derive(Clone)] struct AppState { registry: Arc>>, } async fn handle_any( State(state): State, req: Request, ) -> Result, StatusCode> { let method = req.method().to_string().to_ascii_lowercase(); let uri = req.uri(); // // todo value = Vec let query_params: HashMap = uri .query() .map(|q| { url::form_urlencoded::parse(q.as_bytes()) .into_owned() .collect() }) .unwrap_or_default(); let component = format!("{}/web", &uri.path()); let function_qname = format!("{}/{}", component, method); let mut headers = HashMap::new(); for (k, v) in req.headers().iter() { headers.insert(k.to_string(), v.to_str().unwrap().to_string()); } let path = &req.uri().to_string(); info!("invoked {:?} => {}", req, function_qname); match run_async( state.registry.load(), &function_qname, path, query_params, headers, ) .await { Ok(value) => Ok(Json(value)), Err(_) => { // url checks out but function for method not found if state .registry .load() .get(&format!("{}.main", component)) .is_some() { Err(StatusCode::METHOD_NOT_ALLOWED) } else { Err(StatusCode::NOT_FOUND) } } } }