use anyhow::{Context, Result};
use console::{Term, style};
use serde::Deserialize;
use std::fs;
use std::path::Path;
use std::process::Command;
#[derive(Debug, Deserialize)]
struct CrateInfo {
#[serde(rename = "crate")]
krate: CrateData,
}
#[derive(Debug, Deserialize)]
struct CrateData {
max_version: String,
}
async fn get_latest_version(crate_name: &str) -> Result<String> {
let url = format!("https://crates.io/api/v1/crates/{}", crate_name);
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
let client = reqwest::Client::builder()
.user_agent("audb-cli (https://github.com/tomWhiting/audb)")
.build()?;
let response = client
.get(&url)
.send()
.await
.with_context(|| format!("Failed to fetch version for {}", crate_name))?
.json::<CrateInfo>()
.await
.with_context(|| format!("Failed to parse crates.io response for {}", crate_name))?;
Ok(response.krate.max_version)
}
#[derive(Debug, Clone, Copy)]
enum Template {
Server,
Library,
Cli,
Minimal,
}
impl Template {
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"server" => Ok(Template::Server),
"library" | "lib" => Ok(Template::Library),
"cli" => Ok(Template::Cli),
"minimal" => Ok(Template::Minimal),
_ => anyhow::bail!("Unknown template: {}", s),
}
}
fn description(&self) -> &str {
match self {
Template::Server => "REST API server with Axum",
Template::Library => "Rust library with database access",
Template::Cli => "Command-line tool",
Template::Minimal => "Minimal setup",
}
}
}
pub async fn run(name: &str, template: &str, init_git: bool) -> Result<()> {
let term = Term::stdout();
let template = Template::from_str(template)?;
term.write_line("")?;
term.write_line(&format!(
"{} {}",
style("Creating new AuDB project:").bold().cyan(),
style(name).bold().white()
))?;
term.write_line(&format!(
"{} {}",
style("Template:").dim(),
style(template.description()).white()
))?;
term.write_line("")?;
if Path::new(name).exists() {
anyhow::bail!("Directory '{}' already exists", name);
}
create_project_structure(name, template).await?;
term.write_line(&format!("{} Created project structure", style("✓").green()))?;
generate_gold_files(name, template)?;
term.write_line(&format!("{} Generated gold files", style("✓").green()))?;
create_cargo_toml(name, template).await?;
term.write_line(&format!("{} Created Cargo.toml", style("✓").green()))?;
create_source_files(name, template)?;
term.write_line(&format!("{} Created source files", style("✓").green()))?;
if matches!(
template,
Template::Server | Template::Library | Template::Cli
) {
create_build_script(name)?;
term.write_line(&format!("{} Created build.rs", style("✓").green()))?;
}
if init_git {
init_git_repo(name)?;
term.write_line(&format!(
"{} Initialized Git repository",
style("✓").green()
))?;
}
term.write_line("")?;
term.write_line(&format!(
"{}",
style("Project created successfully!").bold().green()
))?;
term.write_line("")?;
term.write_line(&format!("{}", style("Next steps:").bold()))?;
term.write_line(&format!(" cd {}", name))?;
term.write_line(" cargo build")?;
if matches!(template, Template::Server) {
term.write_line(" cargo run")?;
}
term.write_line("")?;
Ok(())
}
async fn create_project_structure(name: &str, template: Template) -> Result<()> {
fs::create_dir_all(name).context("Failed to create project directory")?;
fs::create_dir_all(format!("{}/gold", name))?;
fs::create_dir_all(format!("{}/src", name))?;
if matches!(
template,
Template::Server | Template::Library | Template::Cli
) {
fs::create_dir_all(format!("{}/src/generated", name))?;
}
Ok(())
}
fn generate_gold_files(name: &str, template: Template) -> Result<()> {
match template {
Template::Server => {
fs::write(format!("{}/gold/config.au", name), SERVER_CONFIG_TEMPLATE)?;
fs::write(format!("{}/gold/schema.au", name), SERVER_SCHEMA_TEMPLATE)?;
}
Template::Library => {
fs::write(format!("{}/gold/schema.au", name), LIBRARY_SCHEMA_TEMPLATE)?;
}
Template::Cli => {
fs::write(format!("{}/gold/schema.au", name), CLI_SCHEMA_TEMPLATE)?;
}
Template::Minimal => {
fs::write(format!("{}/gold/example.au", name), MINIMAL_SCHEMA_TEMPLATE)?;
}
}
Ok(())
}
async fn create_cargo_toml(name: &str, template: Template) -> Result<()> {
let runtime_version = get_latest_version("audb-runtime")
.await
.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch audb-runtime version: {}", e);
"0.1".to_string()
});
let build_version = get_latest_version("audb-build").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch audb-build version: {}", e);
"0.1".to_string()
});
let tokio_version = get_latest_version("tokio").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch tokio version: {}", e);
"1.48".to_string()
});
let axum_version = get_latest_version("axum").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch axum version: {}", e);
"0.8".to_string()
});
let serde_version = get_latest_version("serde").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch serde version: {}", e);
"1.0".to_string()
});
let serde_json_version = get_latest_version("serde_json").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch serde_json version: {}", e);
"1.0".to_string()
});
let openssl_version = get_latest_version("openssl").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch openssl version: {}", e);
"0.10".to_string()
});
let anyhow_version = get_latest_version("anyhow").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch anyhow version: {}", e);
"1.0".to_string()
});
let uuid_version = get_latest_version("uuid").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch uuid version: {}", e);
"1.18".to_string()
});
let chrono_version = get_latest_version("chrono").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch chrono version: {}", e);
"0.4".to_string()
});
let clap_version = get_latest_version("clap").await.unwrap_or_else(|e| {
eprintln!("Warning: Failed to fetch clap version: {}", e);
"4.5".to_string()
});
let content = match template {
Template::Server => format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2024"
[dependencies]
audb-runtime = "{}"
tokio = {{ version = "{}", features = ["full"] }}
axum = "{}"
serde = {{ version = "{}", features = ["derive"] }}
serde_json = "{}"
openssl = {{ version = "{}", features = ["vendored"] }}
anyhow = "{}"
uuid = {{ version = "{}", features = ["v4", "serde"] }}
chrono = {{ version = "{}", features = ["serde"] }}
[build-dependencies]
audb-build = "{}"
"#,
name,
runtime_version,
tokio_version,
axum_version,
serde_version,
serde_json_version,
openssl_version,
anyhow_version,
uuid_version,
chrono_version,
build_version
),
Template::Library => format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2024"
[dependencies]
audb-runtime = "{}"
tokio = {{ version = "{}", features = ["full"] }}
anyhow = "{}"
uuid = {{ version = "{}", features = ["v4", "serde"] }}
chrono = {{ version = "{}", features = ["serde"] }}
openssl = {{ version = "{}", features = ["vendored"] }}
[build-dependencies]
audb-build = "{}"
"#,
name,
runtime_version,
tokio_version,
anyhow_version,
uuid_version,
chrono_version,
openssl_version,
build_version
),
Template::Cli => format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2024"
[dependencies]
audb-runtime = "{}"
tokio = {{ version = "{}", features = ["full"] }}
clap = {{ version = "{}", features = ["derive"] }}
anyhow = "{}"
uuid = {{ version = "{}", features = ["v4", "serde"] }}
openssl = {{ version = "{}", features = ["vendored"] }}
chrono = {{ version = "{}", features = ["serde"] }}
[build-dependencies]
audb-build = "{}"
"#,
name,
runtime_version,
tokio_version,
clap_version,
anyhow_version,
uuid_version,
openssl_version,
chrono_version,
build_version
),
Template::Minimal => format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2024"
[dependencies]
audb-runtime = "{}"
tokio = {{ version = "{}", features = ["full"] }}
anyhow = "{}"
uuid = {{ version = "{}", features = ["v4", "serde"] }}
chrono = {{ version = "{}", features = ["serde"] }}
openssl = {{ version = "{}", features = ["vendored"] }}
[build-dependencies]
audb-build = "{}"
"#,
name,
runtime_version,
tokio_version,
anyhow_version,
uuid_version,
chrono_version,
openssl_version,
build_version
),
};
fs::write(format!("{}/Cargo.toml", name), content)?;
Ok(())
}
fn create_source_files(name: &str, template: Template) -> Result<()> {
match template {
Template::Server => {
fs::write(format!("{}/src/main.rs", name), SERVER_MAIN_TEMPLATE)?;
}
Template::Library => {
fs::write(format!("{}/src/lib.rs", name), LIBRARY_LIB_TEMPLATE)?;
}
Template::Cli => {
fs::write(format!("{}/src/main.rs", name), CLI_MAIN_TEMPLATE)?;
}
Template::Minimal => {
fs::write(format!("{}/src/main.rs", name), MINIMAL_MAIN_TEMPLATE)?;
}
}
Ok(())
}
fn create_build_script(name: &str) -> Result<()> {
fs::write(format!("{}/build.rs", name), BUILD_RS_TEMPLATE)?;
Ok(())
}
fn init_git_repo(name: &str) -> Result<()> {
fs::write(
format!("{}/.gitignore", name),
r#"/target
/src/generated
Cargo.lock
"#,
)?;
Command::new("git")
.args(["init"])
.current_dir(name)
.output()
.context("Failed to initialize Git repository")?;
Ok(())
}
const BUILD_RS_TEMPLATE: &str = r#"fn main() {
audb_build::Builder::new()
.gold_dir("./gold")
.output_dir("./src/generated")
.generate()
.expect("Failed to generate code from gold files");
}
"#;
const SERVER_CONFIG_TEMPLATE: &str = r#"@config database {
path = "./data"
engine = "manifold"
}
@config server {
host = "0.0.0.0"
port = 8080
framework = "axum"
}
"#;
const SERVER_SCHEMA_TEMPLATE: &str = r#"@schema User {
id: EntityId
name: String
email: String
created_at: Timestamp
}
@query get_user(id: EntityId) -> User {
language = "hyperql"
---
SELECT * FROM users WHERE id = :id
---
}
@query list_users(limit: Integer) -> Vec<User> {
language = "hyperql"
---
SELECT * FROM users ORDER BY created_at DESC LIMIT :limit
---
}
@endpoint GET "/api/users/:id" {
query = get_user
auth = false
}
@endpoint GET "/api/users" {
query = list_users
auth = false
}
"#;
const SERVER_MAIN_TEMPLATE: &str = r#"mod generated;
use audb_runtime::Database;
use axum::{routing::get, Router};
use std::net::SocketAddr;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Open database
let db = Database::open("./data").await?;
// Create router
let app = Router::new()
.route("/", get(|| async { "AuDB Server" }));
// Start server
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
println!("Server listening on http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
"#;
const LIBRARY_SCHEMA_TEMPLATE: &str = r#"@schema Item {
id: EntityId
name: String
value: Integer
}
@query get_item(id: EntityId) -> Item {
language = "hyperql"
---
SELECT * FROM items WHERE id = :id
---
}
"#;
const LIBRARY_LIB_TEMPLATE: &str = r#"mod generated;
pub use audb_runtime::{Database, QueryError};
pub use generated::*;
"#;
const CLI_SCHEMA_TEMPLATE: &str = r#"@schema Task {
id: EntityId
title: String
done: Boolean
}
@query list_tasks() -> Vec<Task> {
language = "hyperql"
---
SELECT * FROM tasks ORDER BY id
---
}
"#;
const CLI_MAIN_TEMPLATE: &str = r#"mod generated;
use audb_runtime::Database;
use clap::Parser;
#[derive(Parser)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Parser)]
enum Commands {
List,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let db = Database::open("./data").await?;
match cli.command {
Commands::List => {
println!("Listing items...");
}
}
Ok(())
}
"#;
const MINIMAL_SCHEMA_TEMPLATE: &str = r#"@schema Example {
id: EntityId
name: String
}
"#;
const MINIMAL_MAIN_TEMPLATE: &str = r#"mod generated;
use audb_runtime::Database;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let db = Database::open("./data").await?;
println!("Database opened successfully!");
Ok(())
}
"#;