use rahti_build::Backend;
use crate::templates as t;
pub fn with_dependency(manifest: &str, name: &str, line: &str) -> Option<String> {
if declares(manifest, name) {
return None;
}
Some(append_to_dependencies(manifest, line))
}
pub fn declares(manifest: &str, name: &str) -> bool {
let table = format!("[dependencies.{name}]");
manifest.lines().any(|line| {
let line = line.trim();
line == table
|| line
.strip_prefix(name)
.is_some_and(|rest| rest.trim_start().starts_with('='))
})
}
fn append_to_dependencies(manifest: &str, line: &str) -> String {
let ends_with_newline = manifest.ends_with('\n');
let mut lines: Vec<String> = manifest.lines().map(str::to_string).collect();
match lines.iter().position(|l| l.trim() == "[dependencies]") {
Some(header) => {
let end = lines[header + 1..]
.iter()
.position(|l| l.trim_start().starts_with('['))
.map(|offset| header + 1 + offset)
.unwrap_or(lines.len());
let mut at = end;
while at > header + 1 && lines[at - 1].trim().is_empty() {
at -= 1;
}
lines.insert(at, line.to_string());
}
None => {
if lines.last().is_some_and(|l| !l.trim().is_empty()) {
lines.push(String::new());
}
lines.push("[dependencies]".to_string());
lines.push(line.to_string());
}
}
let mut out = lines.join("\n");
if ends_with_newline {
out.push('\n');
}
out
}
pub fn with_ws_feature(manifest: &str) -> Result<Option<String>, String> {
if manifest.lines().any(|l| l.trim() == "[dependencies.rahti]") {
return Err("`rahti` is declared as a [dependencies.rahti] table".to_string());
}
let at = manifest
.lines()
.position(|l| {
l.trim_start()
.strip_prefix("rahti")
.is_some_and(|rest| rest.trim_start().starts_with('='))
})
.ok_or_else(|| "Cargo.toml has no `rahti` dependency".to_string())?;
let mut lines: Vec<String> = manifest.lines().map(str::to_string).collect();
let line = &lines[at];
let (key, value) = line.split_once('=').expect("the line was found by its `=`");
let value = value.trim();
if value.starts_with('{') && !value.ends_with('}') {
return Err("the `rahti` dependency spans more than one line".to_string());
}
if declares_ws(value) {
return Ok(None);
}
let wired = format!("{key}= {}", t::with_ws_feature(value));
lines[at] = wired;
let mut out = lines.join("\n");
if manifest.ends_with('\n') {
out.push('\n');
}
Ok(Some(out))
}
fn declares_ws(value: &str) -> bool {
value.contains("\"ws\"")
}
pub fn with_version(
manifest: &str,
name: &str,
current: &str,
) -> Result<Option<(String, String)>, String> {
if manifest
.lines()
.any(|l| l.trim() == format!("[dependencies.{name}]"))
{
return Err(format!(
"`{name}` is declared as a [dependencies.{name}] table"
));
}
let Some(at) = manifest.lines().position(|l| {
l.trim_start()
.strip_prefix(name)
.is_some_and(|rest| rest.trim_start().starts_with('='))
}) else {
return Ok(None);
};
let mut lines: Vec<String> = manifest.lines().map(str::to_string).collect();
let line = &lines[at];
let (key, value) = line.split_once('=').expect("the line was found by its `=`");
let value = value.trim();
if value.starts_with('{') && !value.ends_with('}') {
return Err(format!("the `{name}` dependency spans more than one line"));
}
if value.contains("path") {
return Ok(None);
}
let Some(pinned) = pinned_version(value) else {
return Ok(None);
};
if !is_older(&pinned, current) {
return Ok(None);
}
lines[at] = format!("{key}= {}", value.replacen(&pinned, current, 1));
let mut out = lines.join("\n");
if manifest.ends_with('\n') {
out.push('\n');
}
Ok(Some((out, pinned)))
}
fn pinned_version(value: &str) -> Option<String> {
let rest = match value.strip_prefix('{') {
Some(table) => table.split_once("version")?.1.split_once('=')?.1,
None => value,
};
let quoted = rest.trim_start().strip_prefix('"')?;
Some(quoted.split_once('"')?.0.to_string())
}
fn is_older(pinned: &str, current: &str) -> bool {
fn parts(version: &str) -> Option<Vec<u64>> {
version.split('.').map(|p| p.parse::<u64>().ok()).collect()
}
let (Some(was), Some(now)) = (parts(pinned), parts(current)) else {
return false;
};
for i in 0..was.len().max(now.len()) {
let (a, b) = (
was.get(i).copied().unwrap_or(0),
now.get(i).copied().unwrap_or(0),
);
if a != b {
return a < b;
}
}
false
}
pub fn with_database_url(env: &str, backend: Backend) -> Option<String> {
let present = env
.lines()
.any(|l| l.trim_start().starts_with("DATABASE_URL"));
if present {
return None;
}
let mut out = t::database_env(backend);
out.push_str(env);
Some(out)
}
#[cfg(test)]
#[path = "tests/wiring.rs"]
mod tests;