pub(in crate::commands::sdk) fn parse_schema_version(content: &str) -> Option<u64> {
let mut in_project = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
in_project = trimmed == "[project]";
continue;
}
if in_project && trimmed.starts_with("schema_version") {
let raw = trimmed.split('=').nth(1)?.trim();
return raw.parse::<u64>().ok();
}
}
None
}
pub(in crate::commands::sdk) fn parse_project_template(content: &str) -> Option<String> {
let mut in_project = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
in_project = trimmed == "[project]";
continue;
}
if in_project && trimmed.starts_with("template") {
return parse_quoted_assignment(trimmed);
}
}
None
}
pub(in crate::commands::sdk) fn parse_quoted_assignment(line: &str) -> Option<String> {
let value = line.split('=').nth(1)?.trim();
if value.len() < 2 {
return None;
}
if value.starts_with('"') && value.ends_with('"') {
return Some(value[1..value.len() - 1].to_string());
}
None
}
pub(in crate::commands::sdk) fn migrate_robotrt_config_chain(
content: &str,
from_schema_version: u64,
target_schema_version: u64,
fallback_name: &str,
) -> (String, Vec<String>) {
if target_schema_version <= from_schema_version {
return (content.to_string(), Vec::new());
}
let mut out = ensure_project_section(content, fallback_name);
let mut steps = Vec::new();
let mut current = from_schema_version.max(1);
while current < target_schema_version {
let next = current + 1;
out = match next {
2 => {
let upgraded = set_or_insert_key(&out, "project", "schema_version", "2");
steps.push(String::from(
"v1 -> v2: add/normalize [project].schema_version",
));
upgraded
}
3 => {
let with_runtime = set_or_insert_key(&out, "runtime", "profile", "\"balanced\"");
let upgraded = set_or_insert_key(&with_runtime, "project", "schema_version", "3");
steps.push(String::from("v2 -> v3: add [runtime].profile=\"balanced\""));
upgraded
}
4 => {
let with_ops = set_or_insert_key(
&out,
"ops",
"status_api_version",
"\"robotrt.status.service.v1\"",
);
let upgraded = set_or_insert_key(&with_ops, "project", "schema_version", "4");
steps.push(String::from(
"v3 -> v4: add [ops].status_api_version and project schema_version",
));
upgraded
}
version => {
let upgraded =
set_or_insert_key(&out, "project", "schema_version", &version.to_string());
steps.push(format!(
"v{} -> v{}: set [project].schema_version",
current, version
));
upgraded
}
};
current = next;
}
(ensure_trailing_newline(&out), steps)
}
pub(in crate::commands::sdk) fn ensure_project_section(
content: &str,
fallback_name: &str,
) -> String {
if content.trim().is_empty() {
return format!(
"[project]\nname = \"{}\"\ntemplate = \"local\"\nschema_version = 1\n",
fallback_name
);
}
if content.lines().any(|line| line.trim() == "[project]") {
return ensure_trailing_newline(content);
}
let template = parse_project_template(content).unwrap_or_else(|| "local".to_string());
format!(
"{}\n[project]\nname = \"{}\"\ntemplate = \"{}\"\nschema_version = 1\n",
content.trim_end(),
fallback_name,
template
)
}
pub(in crate::commands::sdk) fn set_or_insert_key(
content: &str,
section: &str,
key: &str,
value: &str,
) -> String {
let mut out = Vec::new();
let mut in_target = false;
let mut section_seen = false;
let mut key_seen = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
if in_target && !key_seen {
out.push(format!("{key} = {value}"));
key_seen = true;
}
let section_name = trimmed.trim_matches(&['[', ']'][..]);
in_target = section_name == section;
if in_target {
section_seen = true;
}
out.push(line.to_string());
continue;
}
if in_target && trimmed.starts_with(key) {
out.push(format!("{key} = {value}"));
key_seen = true;
continue;
}
out.push(line.to_string());
}
if in_target && !key_seen {
out.push(format!("{key} = {value}"));
key_seen = true;
}
if !section_seen {
if !out.is_empty() && !out.last().is_some_and(|line| line.is_empty()) {
out.push(String::new());
}
out.push(format!("[{section}]"));
out.push(format!("{key} = {value}"));
} else if !key_seen {
out.push(format!("{key} = {value}"));
}
ensure_trailing_newline(&out.join("\n"))
}
pub(in crate::commands::sdk) fn ensure_trailing_newline(content: &str) -> String {
if content.ends_with('\n') {
content.to_string()
} else {
format!("{content}\n")
}
}