diff --git a/crates/hyperlog-core/src/lib.rs b/crates/hyperlog-core/src/lib.rs index 8326d8f..3102947 100644 --- a/crates/hyperlog-core/src/lib.rs +++ b/crates/hyperlog-core/src/lib.rs @@ -1,4 +1,5 @@ #![feature(map_try_insert)] + pub mod commander; pub mod querier; diff --git a/crates/hyperlog/src/cli.rs b/crates/hyperlog/src/cli.rs new file mode 100644 index 0000000..ac174de --- /dev/null +++ b/crates/hyperlog/src/cli.rs @@ -0,0 +1,116 @@ +use std::{net::SocketAddr, ops::Deref, sync::Arc}; + +use axum::extract::MatchedPath; +use axum::http::Request; +use axum::routing::get; +use axum::Router; +use clap::{Parser, Subcommand}; +use hyperlog_core::{commander, state}; +use tower_http::trace::TraceLayer; + +use crate::{ + server::serve, + state::{SharedState, State}, +}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None, subcommand_required = true)] +struct Command { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + Serve { + #[arg(env = "SERVICE_HOST", long, default_value = "127.0.0.1:3000")] + host: SocketAddr, + }, + Exec { + #[command(subcommand)] + commands: ExecCommands, + }, + Query { + #[command(subcommand)] + commands: QueryCommands, + }, + Info {}, +} + +#[derive(Subcommand)] +enum ExecCommands { + CreateRoot { + #[arg(long = "root")] + root: String, + }, + + CreateSection { + #[arg(long = "root")] + root: String, + + #[arg(long = "path")] + path: Option, + }, +} + +#[derive(Subcommand)] +enum QueryCommands { + Get { + #[arg(long = "root")] + root: String, + + #[arg(long = "path")] + path: Option, + }, +} + +pub async fn execute() -> anyhow::Result<()> { + let cli = Command::parse(); + + let state = state::State::new()?; + + match cli.command { + Some(Commands::Serve { host }) => { + tracing::info!("Starting service"); + + serve(host).await?; + } + Some(Commands::Exec { commands }) => match commands { + ExecCommands::CreateRoot { root } => state + .commander + .execute(commander::Command::CreateRoot { root })?, + ExecCommands::CreateSection { root, path } => { + state.commander.execute(commander::Command::CreateSection { + root, + path: path + .unwrap_or_default() + .split('.') + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .collect::>(), + })? + } + }, + Some(Commands::Query { commands }) => match commands { + QueryCommands::Get { root, path } => { + let res = state.querier.get( + &root, + path.unwrap_or_default() + .split('.') + .filter(|s| !s.is_empty()), + ); + + let output = serde_json::to_string_pretty(&res)?; + + println!("{}", output); + } + }, + Some(Commands::Info {}) => { + println!("graph stored at: {}", state.storage.info()?) + } + + None => {} + } + + Ok(()) +} diff --git a/crates/hyperlog/src/main.rs b/crates/hyperlog/src/main.rs index 1b0c206..552c00a 100644 --- a/crates/hyperlog/src/main.rs +++ b/crates/hyperlog/src/main.rs @@ -1,181 +1,13 @@ -use std::{net::SocketAddr, ops::Deref, sync::Arc}; - -use anyhow::Context; -use axum::extract::MatchedPath; -use axum::http::Request; -use axum::routing::get; -use axum::Router; -use clap::{Parser, Subcommand}; -use hyperlog_core::{commander, state}; -use sqlx::{Pool, Postgres}; -use tower_http::trace::TraceLayer; - -#[derive(Parser)] -#[command(author, version, about, long_about = None, subcommand_required = true)] -struct Command { - #[command(subcommand)] - command: Option, -} - -#[derive(Subcommand)] -enum Commands { - Serve { - #[arg(env = "SERVICE_HOST", long, default_value = "127.0.0.1:3000")] - host: SocketAddr, - }, - Exec { - #[command(subcommand)] - commands: ExecCommands, - }, - Query { - #[command(subcommand)] - commands: QueryCommands, - }, - Info {}, -} - -#[derive(Subcommand)] -enum ExecCommands { - CreateRoot { - #[arg(long = "root")] - root: String, - }, - - CreateSection { - #[arg(long = "root")] - root: String, - - #[arg(long = "path")] - path: Option, - }, -} - -#[derive(Subcommand)] -enum QueryCommands { - Get { - #[arg(long = "root")] - root: String, - - #[arg(long = "path")] - path: Option, - }, -} +mod cli; +pub(crate) mod server; +pub(crate) mod state; #[tokio::main] async fn main() -> anyhow::Result<()> { dotenv::dotenv().ok(); tracing_subscriber::fmt::init(); - let cli = Command::parse(); - - let state = state::State::new()?; - - match cli.command { - Some(Commands::Serve { host }) => { - tracing::info!("Starting service"); - - let state = SharedState(Arc::new(State::new().await?)); - - let app = Router::new() - .route("/", get(root)) - .with_state(state.clone()) - .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); - - tracing::info_span!( - "http_request", - method = ?request.method(), - matched_path, - some_other_field = tracing::field::Empty, - ) - }), // ... - ); - - tracing::info!("listening on {}", host); - let listener = tokio::net::TcpListener::bind(host).await.unwrap(); - axum::serve(listener, app.into_make_service()) - .await - .unwrap(); - } - Some(Commands::Exec { commands }) => match commands { - ExecCommands::CreateRoot { root } => state - .commander - .execute(commander::Command::CreateRoot { root })?, - ExecCommands::CreateSection { root, path } => { - state.commander.execute(commander::Command::CreateSection { - root, - path: path - .unwrap_or_default() - .split('.') - .map(|s| s.to_string()) - .filter(|s| !s.is_empty()) - .collect::>(), - })? - } - }, - Some(Commands::Query { commands }) => match commands { - QueryCommands::Get { root, path } => { - let res = state.querier.get( - &root, - path.unwrap_or_default() - .split('.') - .filter(|s| !s.is_empty()), - ); - - let output = serde_json::to_string_pretty(&res)?; - - println!("{}", output); - } - }, - Some(Commands::Info {}) => { - println!("graph stored at: {}", state.storage.info()?) - } - - None => {} - } + cli::execute().await?; Ok(()) } - -async fn root() -> &'static str { - "Hello, hyperlog!" -} - -#[derive(Clone)] -pub struct SharedState(Arc); - -impl Deref for SharedState { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -pub struct State { - pub db: Pool, -} - -impl State { - pub async fn new() -> anyhow::Result { - let db = sqlx::PgPool::connect( - &std::env::var("DATABASE_URL").context("DATABASE_URL is not set")?, - ) - .await?; - - sqlx::migrate!("migrations/crdb") - .set_locking(false) - .run(&db) - .await?; - - let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?; - - Ok(Self { db }) - } -} diff --git a/crates/hyperlog/src/server.rs b/crates/hyperlog/src/server.rs new file mode 100644 index 0000000..70b5e39 --- /dev/null +++ b/crates/hyperlog/src/server.rs @@ -0,0 +1,42 @@ +use std::{net::SocketAddr, sync::Arc}; + +use axum::{extract::MatchedPath, http::Request, routing::get, Router}; +use tower_http::trace::TraceLayer; + +use crate::state::{self, SharedState, State}; + +async fn root() -> &'static str { + "Hello, hyperlog!" +} + +pub async fn serve(host: SocketAddr) -> anyhow::Result<()> { + let state = SharedState(Arc::new(State::new().await?)); + + let app = Router::new() + .route("/", get(root)) + .with_state(state.clone()) + .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); + + tracing::info_span!( + "http_request", + method = ?request.method(), + matched_path, + some_other_field = tracing::field::Empty, + ) + }), // ... + ); + + tracing::info!("listening on {}", host); + let listener = tokio::net::TcpListener::bind(host).await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); + Ok(()) +} diff --git a/crates/hyperlog/src/state.rs b/crates/hyperlog/src/state.rs new file mode 100644 index 0000000..f008f4a --- /dev/null +++ b/crates/hyperlog/src/state.rs @@ -0,0 +1,37 @@ +use std::{ops::Deref, sync::Arc}; + +use anyhow::Context; +use sqlx::{Pool, Postgres}; + +#[derive(Clone)] +pub struct SharedState(pub Arc); + +impl Deref for SharedState { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct State { + pub db: Pool, +} + +impl State { + pub async fn new() -> anyhow::Result { + let db = sqlx::PgPool::connect( + &std::env::var("DATABASE_URL").context("DATABASE_URL is not set")?, + ) + .await?; + + sqlx::migrate!("migrations/crdb") + .set_locking(false) + .run(&db) + .await?; + + let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?; + + Ok(Self { db }) + } +}