surrealdb-server 3.1.2

A scalable, distributed, collaborative, document-graph database, for the realtime web
Documentation
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};

use anyhow::Result;
use dialoguer::Input;
use semver::Version;
use surrealism_runtime::PrefixErr;
use surrealism_runtime::config::{AbiVersion, SurrealismConfig, SurrealismMeta, Target};

const SURREALISM_VERSION: &str = surrealism_runtime::SDK_VERSION;
const SURREALDB_VERSION: &str = env!("CARGO_PKG_VERSION");

/// Initialize a new Surrealism module project.
pub async fn init(
	path: Option<PathBuf>,
	org: Option<String>,
	name: Option<String>,
	headless: bool,
) -> Result<()> {
	let path = resolve_path(path)?;
	validate_directory(&path)?;

	let dir_name = dir_name_sanitized(&path);

	let org = collect_org(org, headless)?;
	let name = collect_name(name, dir_name, headless)?;

	let target = Target::Rust;

	match target {
		Target::Rust => scaffold_rust(&path, &name)?,
	}

	write_surrealism_toml(&path, target, &org, &name)?;

	println!();
	println!("Surrealism module initialised at {}", path.display());
	println!();
	println!("  Organisation : {org}");
	println!("  Module name  : {name}");
	println!("  Target       : {target}");
	println!();
	println!("Next steps:");
	println!("  cd {}", path.display());
	println!("  surreal module build");
	println!();

	Ok(())
}

/// Resolve the target directory, defaulting to the current working directory.
fn resolve_path(path: Option<PathBuf>) -> Result<PathBuf> {
	match path {
		Some(p) if p.is_absolute() => Ok(p),
		Some(p) => {
			Ok(env::current_dir().prefix_err(|| "Failed to determine current directory")?.join(p))
		}
		None => Ok(env::current_dir().prefix_err(|| "Failed to determine current directory")?),
	}
}

/// Ensure the directory is either new or empty. Allows `.git` and `.gitignore`
/// so that running inside a freshly-cloned repo works.
fn validate_directory(path: &Path) -> Result<()> {
	if !path.exists() {
		fs::create_dir_all(path)
			.prefix_err(|| format!("Failed to create directory: {}", path.display()))?;
		return Ok(());
	}

	if !path.is_dir() {
		anyhow::bail!("Path exists but is not a directory: {}", path.display());
	}

	let has_unexpected_entries = fs::read_dir(path)
		.prefix_err(|| format!("Failed to read directory: {}", path.display()))?
		.filter_map(|e| e.ok())
		.any(|entry| {
			let name = entry.file_name();
			let name = name.to_string_lossy();
			name != ".git" && name != ".gitignore"
		});

	if has_unexpected_entries {
		anyhow::bail!(
			"Directory is not empty: {}\n\
			 Run `surreal module init` in a new or empty directory.",
			path.display()
		);
	}

	Ok(())
}

/// Derive a sanitized default name from the directory's basename. Replaces
/// characters that are invalid in Cargo package names with hyphens.
fn dir_name_sanitized(path: &Path) -> String {
	path.file_name()
		.map(|n| n.to_string_lossy().to_string())
		.unwrap_or_else(|| "my-module".to_string())
		.chars()
		.map(|c| {
			if c.is_alphanumeric() || c == '-' || c == '_' {
				c
			} else {
				'-'
			}
		})
		.collect()
}

fn collect_org(org: Option<String>, headless: bool) -> Result<String> {
	if let Some(org) = org {
		return Ok(org);
	}

	if headless {
		anyhow::bail!("--org is required in headless mode");
	}

	let org: String = Input::new()
		.with_prompt("Organisation name")
		.interact_text()
		.prefix_err(|| "Failed to read organisation name")?;

	if org.trim().is_empty() {
		anyhow::bail!("Organisation name cannot be empty");
	}

	Ok(org.trim().to_string())
}

fn collect_name(name: Option<String>, default: String, headless: bool) -> Result<String> {
	if let Some(name) = name {
		return Ok(name);
	}

	if headless {
		return Ok(default);
	}

	let name: String = Input::new()
		.with_prompt("Module name")
		.default(default)
		.interact_text()
		.prefix_err(|| "Failed to read module name")?;

	if name.trim().is_empty() {
		anyhow::bail!("Module name cannot be empty");
	}

	Ok(name.trim().to_string())
}

// ---------------------------------------------------------------------------
// Rust scaffold
// ---------------------------------------------------------------------------

fn scaffold_rust(path: &Path, name: &str) -> Result<()> {
	run_cargo_init(path, name)?;
	rewrite_cargo_toml(path, name)?;
	write_lib_rs(path)?;
	Ok(())
}

/// Run `cargo init --lib` in the target directory.
fn run_cargo_init(path: &Path, name: &str) -> Result<()> {
	let status = Command::new("cargo")
		.args(["init", "--lib", "--name", name])
		.current_dir(path)
		.status()
		.prefix_err(|| "Failed to execute `cargo init`")?;

	if !status.success() {
		anyhow::bail!("`cargo init` failed");
	}

	Ok(())
}

/// Rewrite the Cargo.toml generated by `cargo init` to add the cdylib
/// crate-type and required dependencies.
fn rewrite_cargo_toml(path: &Path, name: &str) -> Result<()> {
	let cargo_toml = format!(
		r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
anyhow = "1"
surrealism = "{surrealism}"
surrealdb = {{ version = "{surrealdb}", default-features = false }}
surrealdb-types = "{surrealdb}"
"#,
		name = name,
		surrealism = SURREALISM_VERSION,
		surrealdb = SURREALDB_VERSION,
	);

	fs::write(path.join("Cargo.toml"), cargo_toml).prefix_err(|| "Failed to write Cargo.toml")?;

	Ok(())
}

/// Write the starter `src/lib.rs` template.
fn write_lib_rs(path: &Path) -> Result<()> {
	let lib_rs = r#"use surrealism::surrealism;

#[surrealism(default)]
fn hello() -> String {
	format!("Hello from Surrealism!")
}

#[surrealism]
fn greet(name: String) -> String {
	format!("Hello, {name}!")
}

#[surrealism]
fn add(a: i64, b: i64) -> i64 {
	a + b
}
"#;

	fs::write(path.join("src/lib.rs"), lib_rs).prefix_err(|| "Failed to write src/lib.rs")?;

	Ok(())
}

// ---------------------------------------------------------------------------
// surrealism.toml generation (shared across all targets)
// ---------------------------------------------------------------------------

fn write_surrealism_toml(path: &Path, target: Target, org: &str, name: &str) -> Result<()> {
	let config = SurrealismConfig {
		target,
		meta: SurrealismMeta {
			organisation: org.to_string(),
			name: name.to_string(),
			version: Version::new(1, 0, 0),
		},
		capabilities: Default::default(),
		abi: AbiVersion::CURRENT,
		attach: Default::default(),
	};
	let toml = config.to_toml().prefix_err(|| "Failed to serialize surrealism.toml")?;

	fs::write(path.join("surrealism.toml"), toml)
		.prefix_err(|| "Failed to write surrealism.toml")?;

	Ok(())
}