use std::fmt;
use toml_edit::{
ArrayOfTables, Decor, DocumentMut, InlineTable, Item, RawString, Table, TableLike, Value, value,
};
use super::fields::{self, FieldError, FieldSpec, FieldValue, Section, TargetKind};
use super::types::{ListOrAuto, Protocol};
use super::validate::is_valid_name;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
Generator,
Parent,
Embedder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Collection {
Providers,
Targets,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldPath {
Project(&'static str),
Topic {
index: usize,
name: String,
field: &'static str,
},
Provider {
name: String,
field: &'static str,
},
Role {
role: Role,
field: &'static str,
},
Pipeline(&'static str),
Training(&'static str),
Target {
name: String,
field: &'static str,
},
Export(&'static str),
Hub(&'static str),
Metrics(&'static str),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum EditError {
#[error("TOML syntax error in overbrainer.toml at line {line}, column {column}")]
Syntax {
line: usize,
column: usize,
},
#[error("{0}: not found in overbrainer.toml")]
Missing(String),
#[error("{0}: not an editable field")]
NotEditable(String),
#[error("{path}: {reason}")]
Invalid {
path: String,
reason: FieldError,
},
#[error("{0}: already exists")]
Exists(String),
#[error("{0}: name must match ^[a-z0-9_]+$")]
InvalidName(String),
}
#[derive(Debug, Clone)]
pub struct ConfigDoc {
doc: DocumentMut,
}
impl Role {
pub const ALL: [Self; 3] = [Self::Generator, Self::Parent, Self::Embedder];
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Generator => "generator",
Self::Parent => "parent",
Self::Embedder => "embedder",
}
}
}
impl Collection {
#[must_use]
pub fn key(self) -> &'static str {
match self {
Self::Providers => "providers",
Self::Targets => "targets",
}
}
}
impl FieldPath {
#[must_use]
pub fn field(&self) -> &'static str {
match self {
Self::Project(field)
| Self::Pipeline(field)
| Self::Training(field)
| Self::Export(field)
| Self::Hub(field)
| Self::Metrics(field)
| Self::Topic { field, .. }
| Self::Provider { field, .. }
| Self::Role { field, .. }
| Self::Target { field, .. } => field,
}
}
#[must_use]
pub fn table(&self) -> String {
match self {
Self::Project(_) => "project".to_string(),
Self::Topic { name, .. } => format!("topics.{name}"),
Self::Provider { name, .. } => format!("providers.{name}"),
Self::Role { role, .. } => format!("roles.{}", role.as_str()),
Self::Pipeline(_) => "pipeline".to_string(),
Self::Training(_) => "training".to_string(),
Self::Target { name, .. } => format!("targets.{name}"),
Self::Export(_) => "export".to_string(),
Self::Hub(_) => "hub".to_string(),
Self::Metrics(_) => "metrics".to_string(),
}
}
}
impl fmt::Display for FieldPath {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}.{}", self.table(), self.field())
}
}
const NEW_TOPIC_SUBTOPICS: i64 = 10;
const NEW_TOPIC_QUESTIONS: i64 = 30;
const NEW_RUNPOD_MAX_HOURS: f64 = 6.0;
impl ConfigDoc {
pub fn parse(text: &str) -> Result<Self, EditError> {
text.parse::<DocumentMut>()
.map(|doc| Self { doc })
.map_err(|error| syntax(text, error.span().map_or(0, |span| span.start)))
}
#[must_use]
pub fn text(&self) -> String {
self.doc.to_string()
}
#[must_use]
pub fn get(&self, path: &FieldPath) -> Option<String> {
self.spec(path)?;
let value = self.entry(path)?.get(path.field())?.as_value()?;
Some(display(value))
}
#[must_use]
pub fn spec(&self, path: &FieldPath) -> Option<&'static FieldSpec> {
self.spec_of(path).ok()
}
pub fn set(&mut self, path: &FieldPath, value: FieldValue) -> Result<(), EditError> {
let spec = self.spec_of(path)?;
spec.kind
.check(&value)
.map_err(|reason| EditError::Invalid {
path: path.to_string(),
reason,
})?;
self.raw_set(path, value)
}
pub fn unset(&mut self, path: &FieldPath) -> Result<bool, EditError> {
match self.spec_of(path) {
Ok(spec) if !spec.optional => {
return Err(EditError::Invalid {
path: path.to_string(),
reason: FieldError::Required,
});
},
Ok(_) => {},
Err(EditError::Missing(_)) => return Ok(false),
Err(error) => return Err(error),
}
let field = path.field();
let Some(entry) = self.entry_mut(path) else {
return Ok(false);
};
if let Item::Value(Value::InlineTable(table)) = entry {
let last = table.iter().last().is_some_and(|(key, _)| key == field);
let Some(removed) = table.remove(field) else {
return Ok(false);
};
if last
&& let Some((_, value)) = table.iter_mut().last()
&& let Some(suffix) = removed.decor().suffix()
{
value.decor_mut().set_suffix(suffix.clone());
}
return Ok(true);
}
let Item::Table(table) = entry else {
return Ok(entry
.as_table_like_mut()
.is_some_and(|table| table.remove(field).is_some()));
};
let position = table.position();
let moved = table
.key(field)
.map(|key| comment_lines(key.leaf_decor()))
.unwrap_or_default();
let next = table
.iter()
.skip_while(|(key, _)| *key != field)
.skip(1)
.find(|(_, item)| item.is_value())
.map(|(key, _)| key.to_string());
if table.remove(field).is_none() {
return Ok(false);
}
if let Some(mut key) = next.as_deref().and_then(|next| table.key_mut(next)) {
let prefix = raw(key.leaf_decor().prefix());
key.leaf_decor_mut().set_prefix(format!("{moved}{prefix}"));
} else {
self.rehome(position, &moved);
}
Ok(true)
}
#[must_use]
pub fn topic_names(&self) -> Vec<String> {
let Some(topics) = self.doc.get("topics") else {
return Vec::new();
};
let tables: Vec<&dyn TableLike> = match topics {
Item::ArrayOfTables(array) => array.iter().map(|t| t as &dyn TableLike).collect(),
Item::Value(Value::Array(array)) => array
.iter()
.filter_map(|value| value.as_inline_table().map(|t| t as &dyn TableLike))
.collect(),
_ => Vec::new(),
};
tables
.into_iter()
.map(|table| {
table
.get("name")
.and_then(Item::as_str)
.unwrap_or_default()
.to_string()
})
.collect()
}
pub fn add_topic(&mut self, name: &str) -> Result<usize, EditError> {
if self.topic_names().iter().any(|topic| topic == name) {
return Err(EditError::Exists(format!("topics.{name}")));
}
let mut table = Table::new();
table.insert("name", value(name));
table.insert("subtopics", value(NEW_TOPIC_SUBTOPICS));
table.insert("questions_per_subtopic", value(NEW_TOPIC_QUESTIONS));
let root = self.doc.as_table_mut();
match root.get_mut("topics") {
None => {
let mut array = ArrayOfTables::new();
array.push(table);
root.insert("topics", Item::ArrayOfTables(array));
self.renumber();
Ok(0)
},
Some(Item::ArrayOfTables(array)) => {
array.push(table);
let index = array.len() - 1;
self.renumber();
Ok(index)
},
Some(Item::Value(Value::Array(array))) => {
array.push(table.into_inline_table());
Ok(array.len() - 1)
},
Some(_) => Err(EditError::Missing("topics".to_string())),
}
}
pub fn remove_topic(&mut self, index: usize) -> bool {
let root = self.doc.as_table_mut();
let (remaining, removed) = match root.get_mut("topics") {
Some(Item::ArrayOfTables(array)) if index < array.len() => {
let removed = array.remove(index);
let comments = comment_lines(removed.decor());
if let Some(next) = array.get_mut(index) {
prepend_comments(next, &comments);
return true;
}
(array.len(), Some(removed))
},
Some(Item::Value(Value::Array(array))) if index < array.len() => {
array.remove(index);
(array.len(), None)
},
_ => return false,
};
if remaining == 0 {
root.remove("topics");
}
if let Some(removed) = removed {
self.rehome(removed.position(), &comment_lines(removed.decor()));
}
true
}
#[must_use]
pub fn names(&self, collection: Collection) -> Vec<String> {
self.doc
.get(collection.key())
.and_then(Item::as_table_like)
.map(|table| table.iter().map(|(key, _)| key.to_string()).collect())
.unwrap_or_default()
}
#[must_use]
pub fn target_kind(&self, name: &str) -> Option<TargetKind> {
let kind = self.doc.get("targets")?.get(name)?.get("kind")?.as_str()?;
TargetKind::from_name(kind)
}
pub fn add_provider(&mut self, name: &str, protocol: Protocol) -> Result<(), EditError> {
let mut table = Table::new();
table.insert("protocol", value(protocol_name(protocol)));
self.add_named(Collection::Providers, name, table)
}
pub fn add_target(&mut self, name: &str, kind: TargetKind) -> Result<(), EditError> {
let mut table = Table::new();
table.insert("kind", value(kind.as_str()));
match kind {
TargetKind::Local | TargetKind::Ssh => {
table.insert("runtime", value("docker"));
},
TargetKind::Runpod => {
table.insert("gpu_types", value(ListOrAuto::AUTO));
table.insert("gpu_count", value(1));
table.insert("container_disk_gb", value(50));
table.insert("max_hours", value(NEW_RUNPOD_MAX_HOURS));
table.insert("boot_grace_minutes", value(30));
table.insert("retrieve_grace_minutes", value(60));
},
}
self.add_named(Collection::Targets, name, table)
}
pub fn remove_table(&mut self, collection: Collection, name: &str) -> bool {
let root = self.doc.as_table_mut();
let Some(tables) = root
.get_mut(collection.key())
.and_then(Item::as_table_like_mut)
else {
return false;
};
let next = tables
.iter()
.skip_while(|(key, _)| *key != name)
.skip(1)
.find(|(_, item)| item.is_table())
.map(|(key, _)| key.to_string());
let Some(removed) = tables.remove(name) else {
return false;
};
let Item::Table(removed) = removed else {
if tables.is_empty() {
root.remove(collection.key());
}
return true;
};
let comments = comment_lines(removed.decor());
if let Some(next) = next
.as_deref()
.and_then(|next| tables.get_mut(next))
.and_then(Item::as_table_mut)
{
prepend_comments(next, &comments);
return true;
}
if tables.is_empty() {
root.remove(collection.key());
}
self.rehome(removed.position(), &comments);
true
}
fn rehome(&mut self, after: Option<isize>, comments: &str) {
if comments.is_empty() {
return;
}
let next = after.and_then(|after| {
self.tables()
.into_iter()
.filter(|table| table.headed && table.position > after)
.min_by_key(|table| table.position)
});
if let Some(table) = next.and_then(|table| self.table_at(&table.steps)) {
prepend_comments(table, comments);
} else {
let trailing = raw(Some(self.doc.trailing()));
self.doc.set_trailing(format!("{trailing}\n{comments}"));
}
}
fn tables(&self) -> Vec<Located> {
let mut found = Vec::new();
tables_under(self.doc.as_table(), &mut Vec::new(), &mut 0, &mut found);
found
}
fn renumber(&mut self) {
let mut tables = self.tables();
tables.sort_by_key(|table| table.position);
for (rank, located) in (1..).zip(tables) {
if let Some(table) = self.table_at(&located.steps) {
table.set_position(Some(rank));
}
}
}
fn table_at(&mut self, steps: &[Step]) -> Option<&mut Table> {
let mut item = self.doc.as_item_mut();
for step in steps {
item = match step {
Step::Key(key) => item.get_mut(key.as_str())?,
Step::Index(index) => item.get_mut(*index)?,
};
}
item.as_table_mut()
}
fn add_named(
&mut self,
collection: Collection,
name: &str,
table: Table,
) -> Result<(), EditError> {
if !is_valid_name(name) {
return Err(EditError::InvalidName(name.to_string()));
}
let key = collection.key();
let root = self.doc.as_table_mut();
if !root.contains_key(key) {
let mut parent = Table::new();
parent.set_implicit(true);
root.insert(key, Item::Table(parent));
}
let tables = root
.get_mut(key)
.and_then(Item::as_table_like_mut)
.ok_or_else(|| EditError::Missing(key.to_string()))?;
if tables.contains_key(name) {
return Err(EditError::Exists(format!("{key}.{name}")));
}
tables.insert(name, Item::Table(table));
self.renumber();
Ok(())
}
fn spec_of(&self, path: &FieldPath) -> Result<&'static FieldSpec, EditError> {
let section = match path {
FieldPath::Project(_) => Section::Project,
FieldPath::Topic { .. } => Section::Topic,
FieldPath::Provider { .. } => Section::Provider,
FieldPath::Role { .. } => Section::Role,
FieldPath::Pipeline(_) => Section::Pipeline,
FieldPath::Training(_) => Section::Training,
FieldPath::Export(_) => Section::Export,
FieldPath::Hub(_) => Section::Hub,
FieldPath::Metrics(_) => Section::Metrics,
FieldPath::Target { name, .. } => {
if self.entry(path).is_none() {
return Err(EditError::Missing(format!("targets.{name}")));
}
self.target_kind(name)
.map(Section::Target)
.ok_or_else(|| EditError::NotEditable(path.to_string()))?
},
};
fields::find(section, path.field()).ok_or_else(|| EditError::NotEditable(path.to_string()))
}
fn raw_set(&mut self, path: &FieldPath, value: FieldValue) -> Result<(), EditError> {
let missing = || EditError::Missing(path.table());
let created = self.entry(path).is_none();
let entry = self.entry_or_create(path).ok_or_else(missing)?;
let mut new = toml_value(value);
if let Item::Value(Value::InlineTable(table)) = entry
&& !table.contains_key(path.field())
{
if let Some((_, last)) = table.iter_mut().last() {
let suffix = last.decor().suffix().cloned();
last.decor_mut().set_suffix("");
if let Some(suffix) = suffix {
new.decor_mut().set_suffix(suffix);
}
}
table.insert(path.field(), new);
return Ok(());
}
let table = entry.as_table_like_mut().ok_or_else(missing)?;
match table.get_mut(path.field()) {
Some(item) => {
if let Some(old) = item.as_value() {
*new.decor_mut() = old.decor().clone();
}
*item = Item::Value(new);
},
None => {
table.insert(path.field(), Item::Value(new));
},
}
if created {
self.renumber();
}
Ok(())
}
fn entry(&self, path: &FieldPath) -> Option<&Item> {
let root = self.doc.as_item();
match path {
FieldPath::Project(_) => root.get("project"),
FieldPath::Topic { index, name, .. } => root
.get("topics")?
.get(*index)
.filter(|topic| topic.get("name").and_then(Item::as_str) == Some(name)),
FieldPath::Provider { name, .. } => root.get("providers")?.get(name.as_str()),
FieldPath::Role { role, .. } => root.get("roles")?.get(role.as_str()),
FieldPath::Pipeline(_) => root.get("pipeline"),
FieldPath::Training(_) => root.get("training"),
FieldPath::Export(_) => root.get("export"),
FieldPath::Hub(_) => root.get("hub"),
FieldPath::Metrics(_) => root.get("metrics"),
FieldPath::Target { name, .. } => root.get("targets")?.get(name.as_str()),
}
}
fn entry_mut(&mut self, path: &FieldPath) -> Option<&mut Item> {
let root = self.doc.as_item_mut();
match path {
FieldPath::Project(_) => root.get_mut("project"),
FieldPath::Topic { index, name, .. } => root
.get_mut("topics")?
.get_mut(*index)
.filter(|topic| topic.get("name").and_then(Item::as_str) == Some(name)),
FieldPath::Provider { name, .. } => root.get_mut("providers")?.get_mut(name.as_str()),
FieldPath::Role { role, .. } => root.get_mut("roles")?.get_mut(role.as_str()),
FieldPath::Pipeline(_) => root.get_mut("pipeline"),
FieldPath::Training(_) => root.get_mut("training"),
FieldPath::Export(_) => root.get_mut("export"),
FieldPath::Hub(_) => root.get_mut("hub"),
FieldPath::Metrics(_) => root.get_mut("metrics"),
FieldPath::Target { name, .. } => root.get_mut("targets")?.get_mut(name.as_str()),
}
}
fn entry_or_create(&mut self, path: &FieldPath) -> Option<&mut Item> {
if matches!(
path,
FieldPath::Topic { .. } | FieldPath::Provider { .. } | FieldPath::Target { .. }
) {
return self.entry_mut(path);
}
let root = self.doc.as_table_mut();
match path {
FieldPath::Project(_) => ensure_table(root, "project"),
FieldPath::Pipeline(_) => ensure_table(root, "pipeline"),
FieldPath::Training(_) => ensure_table(root, "training"),
FieldPath::Export(_) => ensure_table(root, "export"),
FieldPath::Hub(_) => ensure_table(root, "hub"),
FieldPath::Metrics(_) => ensure_table(root, "metrics"),
FieldPath::Role { role, .. } => {
let roles = ensure_table(root, "roles")?.as_table_like_mut()?;
let key = role.as_str();
if !roles.contains_key(key) {
let inline = roles.iter().any(|(_, item)| item.is_inline_table());
let entry = if inline {
Item::Value(Value::InlineTable(InlineTable::new()))
} else {
Item::Table(Table::new())
};
roles.insert(key, entry);
}
roles.get_mut(key)
},
FieldPath::Topic { .. } | FieldPath::Provider { .. } | FieldPath::Target { .. } => None,
}
}
}
fn ensure_table<'a>(root: &'a mut Table, key: &str) -> Option<&'a mut Item> {
if !root.contains_key(key) {
root.insert(key, Item::Table(Table::new()));
}
root.get_mut(key)
}
fn syntax(text: &str, offset: usize) -> EditError {
let mut end = offset.min(text.len());
while !text.is_char_boundary(end) {
end -= 1;
}
let before = text.get(..end).unwrap_or_default();
let line_start = before.rfind('\n').map_or(0, |index| index + 1);
EditError::Syntax {
line: before.matches('\n').count() + 1,
column: before[line_start..].chars().count() + 1,
}
}
#[derive(Debug, Clone)]
enum Step {
Key(String),
Index(usize),
}
struct Located {
steps: Vec<Step>,
position: isize,
headed: bool,
}
fn tables_under(table: &Table, steps: &mut Vec<Step>, last: &mut isize, found: &mut Vec<Located>) {
for (key, item) in table {
let children: Vec<(Option<usize>, &Table)> = match item {
Item::Table(child) => vec![(None, child)],
Item::ArrayOfTables(array) => array
.iter()
.enumerate()
.map(|(index, child)| (Some(index), child))
.collect(),
_ => continue,
};
for (index, child) in children {
steps.push(Step::Key(key.to_string()));
if let Some(index) = index {
steps.push(Step::Index(index));
}
if !child.is_dotted() {
*last = child.position().unwrap_or(*last);
found.push(Located {
steps: steps.clone(),
position: *last,
headed: !child.is_implicit(),
});
}
tables_under(child, steps, last, found);
steps.truncate(steps.len() - 1 - usize::from(index.is_some()));
}
}
}
fn prepend_comments(table: &mut Table, comments: &str) {
if comments.is_empty() {
return;
}
let prefix = table
.decor()
.prefix()
.map_or_else(|| "\n".to_string(), |prefix| raw(Some(prefix)));
let prefix = match prefix.strip_prefix('\n') {
Some(rest) => format!("\n{comments}{rest}"),
None => format!("{comments}{prefix}"),
};
table.decor_mut().set_prefix(prefix);
}
fn comment_lines(decor: &Decor) -> String {
let mut comments = String::new();
for line in raw(decor.prefix()).lines() {
if line.trim_start().starts_with('#') {
comments.push_str(line);
comments.push('\n');
}
}
comments
}
fn raw(text: Option<&RawString>) -> String {
text.and_then(RawString::as_str)
.unwrap_or_default()
.to_string()
}
fn protocol_name(protocol: Protocol) -> &'static str {
match protocol {
Protocol::Openai => "openai",
Protocol::Anthropic => "anthropic",
}
}
fn toml_value(value: FieldValue) -> Value {
match value {
FieldValue::Text(text) => Value::from(text),
FieldValue::Int(number) => Value::from(number),
FieldValue::Float(number) => Value::from(number),
FieldValue::Bool(flag) => Value::from(flag),
FieldValue::List(items) => Value::Array(items.into_iter().collect()),
}
}
fn display(value: &Value) -> String {
match value {
Value::String(text) => text.value().clone(),
Value::Integer(number) => number.value().to_string(),
Value::Float(number) => format!("{:?}", number.value()),
Value::Boolean(flag) => flag.value().to_string(),
Value::Array(items) => items.iter().map(display).collect::<Vec<_>>().join(", "),
Value::Datetime(_) | Value::InlineTable(_) => value.to_string().trim().to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::fields::{Bound, FieldKind};
use crate::config::{ConfigError, EnvSource, load_str};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const COMMENTED: &str = r#"# overbrainer project
[project]
name = "demo" # the project
# What to ask about
[[topics]]
name = "ownership"
subtopics = 3 # a few
questions_per_subtopic = 5
# Providers: keys come from env
[providers.nanogpt]
protocol = "openai"
[roles]
# the question writer
generator = { provider = "nanogpt", model = "m1" }
parent = { provider = "nanogpt", model = "m2", reasoning = true }
[pipeline]
concurrency = 8 # parallel requests
seed = 42
[training]
target = "local"
base_model = "Qwen/Qwen3-4B"
adapter = "qlora"
epochs = 3 # one pass is too few
# Where training runs
[targets.local]
kind = "local"
runtime = "native"
"#;
fn validate(doc: &ConfigDoc) -> Result<(), ConfigError> {
load_str(&doc.text(), EnvSource::Vars(Vec::new())).map(|_| ())
}
fn problems(doc: &ConfigDoc) -> Vec<String> {
match load_str(&doc.text(), EnvSource::Vars(Vec::new())) {
Err(ConfigError::Invalid(problems)) => problems,
Err(ConfigError::Parse(message)) => vec![message],
Err(ConfigError::Read { .. }) | Ok(_) => Vec::new(),
}
}
#[test]
fn an_untouched_document_keeps_its_text() -> TestResult {
let doc = ConfigDoc::parse(COMMENTED)?;
assert_eq!(doc.text(), COMMENTED);
validate(&doc)?;
Ok(())
}
#[test]
fn a_round_trip_keeps_comments_and_key_order() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
doc.set(&FieldPath::Pipeline("concurrency"), FieldValue::Int(16))?;
let model = FieldPath::Role {
role: Role::Generator,
field: "model",
};
doc.set(&model, FieldValue::Text("m3".to_string()))?;
assert!(doc.unset(&FieldPath::Training("epochs"))?);
let text = doc.text();
for line in COMMENTED
.lines()
.filter(|line| line.trim_start().starts_with('#'))
{
assert!(text.contains(line), "lost comment {line:?} in:\n{text}");
}
assert!(text.contains("concurrency = 16 # parallel requests\nseed = 42"));
assert!(text.contains("generator = { provider = \"nanogpt\", model = \"m3\" }"));
assert!(!text.contains("epochs"));
assert!(!text.contains("one pass is too few"));
assert_eq!(doc.get(&model), Some("m3".to_string()));
validate(&doc)?;
Ok(())
}
#[test]
fn setting_an_absent_optional_adds_it_in_its_table() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
doc.set(
&FieldPath::Training("hub_model_id"),
FieldValue::Text("me/child".to_string()),
)?;
let text = doc.text();
let training = text.find("[training]").ok_or("no [training]")?;
let key = text.find("hub_model_id = \"me/child\"").ok_or("no key")?;
let next = text.find("# Where training runs").ok_or("no targets")?;
assert!(training < key && key < next, "misplaced key in:\n{text}");
let effort = FieldPath::Role {
role: Role::Parent,
field: "reasoning_effort",
};
doc.set(&effort, FieldValue::Text("high".to_string()))?;
assert!(doc.text().contains(
"parent = { provider = \"nanogpt\", model = \"m2\", reasoning = true, reasoning_effort = \"high\" }"
));
validate(&doc)?;
Ok(())
}
#[test]
fn the_metrics_address_is_written_and_validated() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
let listen = FieldPath::Metrics("listen");
assert_eq!(listen.to_string(), "metrics.listen");
doc.set(&listen, FieldValue::Text("127.0.0.1:9464".to_string()))?;
assert!(
doc.text()
.contains("[metrics]\nlisten = \"127.0.0.1:9464\"\n")
);
validate(&doc)?;
doc.set(&listen, FieldValue::Text("localhost".to_string()))?;
let found = problems(&doc);
assert!(
found
.iter()
.any(|problem| problem.contains("metrics.listen")),
"{found:?}"
);
Ok(())
}
#[test]
fn setting_in_an_absent_section_creates_it() -> TestResult {
let base = COMMENTED.replace(
"[pipeline]\nconcurrency = 8 # parallel requests\nseed = 42\n\n",
"",
);
let mut doc = ConfigDoc::parse(&base)?;
doc.set(&FieldPath::Pipeline("eval_ratio"), FieldValue::Float(0.2))?;
assert!(doc.text().contains("[pipeline]\neval_ratio = 0.2\n"));
let embedder = |field| FieldPath::Role {
role: Role::Embedder,
field,
};
doc.set(
&embedder("provider"),
FieldValue::Text("nanogpt".to_string()),
)?;
doc.set(&embedder("model"), FieldValue::Text("e1".to_string()))?;
assert!(
doc.text()
.contains("embedder = { provider = \"nanogpt\", model = \"e1\" }")
);
validate(&doc)?;
Ok(())
}
#[test]
fn named_entries_must_exist_before_a_set() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
let path = FieldPath::Provider {
name: "missing".to_string(),
field: "protocol",
};
assert_eq!(
doc.set(&path, FieldValue::Text("openai".to_string())),
Err(EditError::Missing("providers.missing".to_string()))
);
Ok(())
}
#[test]
fn only_editable_fields_with_valid_values_are_set() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
let key = FieldPath::Provider {
name: "nanogpt".to_string(),
field: "api_key",
};
assert_eq!(
doc.set(&key, FieldValue::Text("sk-secret".to_string())),
Err(EditError::NotEditable(
"providers.nanogpt.api_key".to_string()
))
);
let other_kind = FieldPath::Target {
name: "local".to_string(),
field: "gpu_types",
};
assert!(matches!(
doc.set(&other_kind, FieldValue::List(Vec::new())),
Err(EditError::NotEditable(_))
));
doc.add_target("box", TargetKind::Ssh)?;
let host = FieldPath::Target {
name: "box".to_string(),
field: "host",
};
assert_eq!(
doc.set(&host, FieldValue::Text("me@gpu".to_string())),
Err(EditError::NotEditable("targets.box.host".to_string()))
);
assert_eq!(
doc.unset(&FieldPath::Training("target")),
Err(EditError::Invalid {
path: "training.target".to_string(),
reason: FieldError::Required,
})
);
assert!(doc.remove_table(Collection::Targets, "box"));
let refused = [
(FieldPath::Pipeline("concurrency"), FieldValue::Int(0)),
(
FieldPath::Pipeline("concurrency"),
FieldValue::Text("8".to_string()),
),
(
FieldPath::Training("adapter"),
FieldValue::Text("x".to_string()),
),
(
FieldPath::Pipeline("eval_ratio"),
FieldValue::Float(f64::NAN),
),
];
for (path, value) in refused {
let error = doc.set(&path, value);
assert!(
matches!(&error, Err(EditError::Invalid { path: p, .. }) if *p == path.to_string()),
"{path}: {error:?}"
);
}
assert_eq!(doc.text(), COMMENTED);
Ok(())
}
#[test]
fn unsetting_the_last_key_of_an_inline_table_keeps_its_spacing() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
let reasoning = FieldPath::Role {
role: Role::Parent,
field: "reasoning",
};
assert!(doc.unset(&reasoning)?);
assert!(!doc.unset(&reasoning)?);
assert!(
doc.text()
.contains("parent = { provider = \"nanogpt\", model = \"m2\" }\n")
);
let absent = FieldPath::Target {
name: "cloud".to_string(),
field: "image",
};
assert!(!doc.unset(&absent)?);
Ok(())
}
#[test]
fn a_new_runpod_target_writes_the_serde_defaults() -> TestResult {
let minimal = format!(
"{COMMENTED}\n[targets.cloud]\nkind = \"runpod\"\ngpu_types = [\"A40\"]\nmax_hours = 6.0\n"
);
let mut doc = ConfigDoc::parse(COMMENTED)?;
doc.add_target("cloud", TargetKind::Runpod)?;
let gpus = FieldPath::Target {
name: "cloud".to_string(),
field: "gpu_types",
};
doc.set(&gpus, FieldValue::List(vec!["A40".to_string()]))?;
let written = load_str(&doc.text(), EnvSource::Vars(Vec::new()))?;
let defaults = load_str(&minimal, EnvSource::Vars(Vec::new()))?;
assert_eq!(
format!("{:?}", written.targets.get("cloud")),
format!("{:?}", defaults.targets.get("cloud"))
);
Ok(())
}
#[test]
fn a_stray_secret_in_the_file_is_never_returned() -> TestResult {
let text = COMMENTED.replace(
"protocol = \"openai\"\n",
"protocol = \"openai\"\napi_key = \"sk-secret\"\n",
);
let doc = ConfigDoc::parse(&text)?;
let key = FieldPath::Provider {
name: "nanogpt".to_string(),
field: "api_key",
};
assert_eq!(doc.get(&key), None);
assert_eq!(doc.spec(&key), None);
Ok(())
}
#[test]
fn a_topic_path_must_match_the_name_at_its_index() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
let stale = FieldPath::Topic {
index: 0,
name: "traits".to_string(),
field: "subtopics",
};
assert_eq!(doc.get(&stale), None);
assert_eq!(
doc.set(&stale, FieldValue::Int(4)),
Err(EditError::Missing("topics.traits".to_string()))
);
let description = FieldPath::Topic {
index: 0,
name: "traits".to_string(),
field: "description",
};
assert!(!doc.unset(&description)?);
assert_eq!(doc.text(), COMMENTED);
Ok(())
}
#[test]
fn removals_keep_the_comments_above_them() -> TestResult {
let text = COMMENTED.replace("seed = 42\n", "# the split\nseed = 42\n");
let mut doc = ConfigDoc::parse(&text)?;
assert!(doc.unset(&FieldPath::Pipeline("seed"))?);
assert!(
doc.text()
.contains("concurrency = 8 # parallel requests\n\n# the split\n[training]")
);
let text = text.replace("adapter = \"qlora\"\n", "adapter = \"qlora\"\n# rounds\n");
let mut doc = ConfigDoc::parse(&text)?;
assert!(doc.unset(&FieldPath::Training("epochs"))?);
assert!(doc.text().contains("[training]\ntarget"));
let mut doc = ConfigDoc::parse(COMMENTED)?;
doc.add_topic("traits")?;
assert!(doc.remove_topic(0));
assert!(
doc.text()
.contains("# the project\n\n# What to ask about\n[[topics]]\nname = \"traits\"")
);
assert!(doc.remove_topic(0));
assert!(
doc.text().contains(
"# What to ask about\n# Providers: keys come from env\n[providers.nanogpt]"
)
);
assert!(doc.remove_table(Collection::Providers, "nanogpt"));
assert!(
doc.text()
.contains("# What to ask about\n# Providers: keys come from env\n[roles]")
);
assert!(doc.remove_table(Collection::Targets, "local"));
assert!(
doc.text()
.ends_with("epochs = 3 # one pass is too few\n\n# Where training runs\n")
);
validate(&ConfigDoc::parse(&doc.text())?).err();
let mut doc = ConfigDoc::parse(COMMENTED)?;
doc.add_provider("claude", Protocol::Anthropic)?;
assert!(doc.remove_table(Collection::Providers, "nanogpt"));
assert!(
doc.text()
.contains("# Providers: keys come from env\n[providers.claude]")
);
Ok(())
}
#[test]
fn get_shows_values_in_display_form() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
assert_eq!(
doc.get(&FieldPath::Project("name")),
Some("demo".to_string())
);
assert_eq!(
doc.get(&FieldPath::Pipeline("concurrency")),
Some("8".to_string())
);
assert_eq!(doc.get(&FieldPath::Pipeline("eval_ratio")), None);
let reasoning = FieldPath::Role {
role: Role::Parent,
field: "reasoning",
};
assert_eq!(doc.get(&reasoning), Some("true".to_string()));
doc.add_target("cloud", TargetKind::Runpod)?;
let gpus = FieldPath::Target {
name: "cloud".to_string(),
field: "gpu_types",
};
doc.set(
&gpus,
FieldValue::List(vec!["A40".to_string(), "L40S".to_string()]),
)?;
assert_eq!(doc.get(&gpus), Some("A40, L40S".to_string()));
let hours = FieldPath::Target {
name: "cloud".to_string(),
field: "max_hours",
};
assert_eq!(doc.get(&hours), Some("6.0".to_string()));
Ok(())
}
#[test]
fn topics_are_added_and_removed() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
assert_eq!(doc.add_topic("traits")?, 1);
assert_eq!(
doc.add_topic("traits"),
Err(EditError::Exists("topics.traits".to_string()))
);
assert_eq!(doc.topic_names(), vec!["ownership", "traits"]);
let text = doc.text();
let first = text.find("name = \"ownership\"").ok_or("no first")?;
let second = text.find("name = \"traits\"").ok_or("no second")?;
let providers = text.find("# Providers").ok_or("no providers")?;
assert!(
first < second && second < providers,
"misplaced topic in:\n{text}"
);
validate(&doc)?;
assert!(doc.remove_topic(0));
assert!(!doc.remove_topic(5));
assert_eq!(doc.topic_names(), vec!["traits"]);
let subtopics = FieldPath::Topic {
index: 0,
name: "traits".to_string(),
field: "subtopics",
};
assert_eq!(doc.get(&subtopics), Some("10".to_string()));
assert!(doc.text().contains("# Providers: keys come from env"));
validate(&doc)?;
assert!(doc.remove_topic(0));
assert_eq!(doc.topic_names(), [] as [String; 0]);
assert!(!doc.text().contains("[[topics]]"));
Ok(())
}
#[test]
fn providers_and_targets_are_added_after_their_kind() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
doc.add_provider("claude", Protocol::Anthropic)?;
assert_eq!(
doc.add_provider("claude", Protocol::Openai),
Err(EditError::Exists("providers.claude".to_string()))
);
assert_eq!(
doc.add_provider("Bad-Name", Protocol::Openai),
Err(EditError::InvalidName("Bad-Name".to_string()))
);
doc.add_target("cloud", TargetKind::Runpod)?;
doc.add_target("box", TargetKind::Ssh)?;
let text = doc.text();
let nanogpt = text.find("[providers.nanogpt]").ok_or("no nanogpt")?;
let claude = text
.find("[providers.claude]\nprotocol = \"anthropic\"\n")
.ok_or("no claude")?;
let roles = text.find("[roles]").ok_or("no roles")?;
assert!(
nanogpt < claude && claude < roles,
"misplaced provider in:\n{text}"
);
let local = text.find("[targets.local]").ok_or("no local")?;
let cloud = text.find("[targets.cloud]").ok_or("no cloud")?;
let then = text.find("[targets.box]").ok_or("no box")?;
assert!(
local < cloud && cloud < then,
"misplaced target in:\n{text}"
);
assert!(text.contains(
"[targets.cloud]\nkind = \"runpod\"\ngpu_types = \"auto\"\ngpu_count = 1\ncontainer_disk_gb = 50\nmax_hours = 6.0\nboot_grace_minutes = 30\nretrieve_grace_minutes = 60\n"
));
assert!(text.contains("[targets.box]\nkind = \"ssh\"\nruntime = \"docker\"\n"));
assert_eq!(doc.target_kind("cloud"), Some(TargetKind::Runpod));
assert_eq!(doc.names(Collection::Providers), vec!["nanogpt", "claude"]);
validate(&doc)?;
let gpus = FieldPath::Target {
name: "cloud".to_string(),
field: "gpu_types",
};
doc.set(&gpus, FieldValue::List(vec!["NVIDIA A40".to_string()]))?;
validate(&doc)?;
Ok(())
}
#[test]
fn a_first_provider_or_target_creates_its_collection() -> TestResult {
let mut doc = ConfigDoc::parse("[project]\nname = \"demo\"\n")?;
doc.add_provider("nanogpt", Protocol::Openai)?;
doc.add_target("local", TargetKind::Local)?;
assert_eq!(doc.add_topic("ownership")?, 0);
let text = doc.text();
assert!(!text.contains("[providers]\n"), "bare header in:\n{text}");
assert!(text.contains("[providers.nanogpt]\nprotocol = \"openai\"\n"));
assert!(text.contains("[targets.local]\nkind = \"local\"\nruntime = \"docker\"\n"));
assert!(text.contains(
"[[topics]]\nname = \"ownership\"\nsubtopics = 10\nquestions_per_subtopic = 30\n"
));
Ok(())
}
#[test]
fn removing_a_table_keeps_the_rest() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
doc.add_provider("claude", Protocol::Anthropic)?;
assert!(doc.remove_table(Collection::Providers, "nanogpt"));
assert!(!doc.remove_table(Collection::Providers, "nanogpt"));
assert!(!doc.remove_table(Collection::Targets, "cloud"));
let text = doc.text();
assert!(!text.contains("nanogpt]"));
assert!(text.contains("[providers.claude]"));
assert!(text.contains("# Where training runs\n[targets.local]"));
assert!(text.contains("# the question writer"));
assert!(doc.remove_table(Collection::Targets, "local"));
assert!(!doc.text().contains("[targets.local]"));
Ok(())
}
#[test]
fn a_syntax_error_names_only_its_location() {
let error = ConfigDoc::parse("[project]\nname = \"sk-secret\nx = 1\n");
assert!(
matches!(error, Err(EditError::Syntax { line: 2, .. })),
"{error:?}"
);
let message = error
.map(|_| ())
.err()
.map(|e| e.to_string())
.unwrap_or_default();
assert!(!message.contains("sk-secret"));
assert_eq!(syntax("é\nx", 1), EditError::Syntax { line: 1, column: 1 });
assert_eq!(syntax("a\né", 9), EditError::Syntax { line: 2, column: 2 });
}
#[test]
fn paths_display_as_validate_names_them() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
doc.add_target("cloud", TargetKind::Runpod)?;
let cases = [
(
FieldPath::Topic {
index: 0,
name: "ownership".to_string(),
field: "subtopics",
},
FieldValue::Int(0),
),
(
FieldPath::Role {
role: Role::Generator,
field: "max_tokens",
},
FieldValue::Int(0),
),
(
FieldPath::Pipeline("question_batch_size"),
FieldValue::Int(0),
),
(
FieldPath::Training("lr_scheduler"),
FieldValue::Text(" ".to_string()),
),
(
FieldPath::Target {
name: "cloud".to_string(),
field: "gpu_types",
},
FieldValue::List(Vec::new()),
),
];
for (path, value) in cases {
let mut edited = doc.clone();
edited.raw_set(&path, value)?;
let problems = problems(&edited);
let prefix = format!("{path}:");
assert!(
problems.iter().any(|problem| problem.starts_with(&prefix)),
"{prefix} not in {problems:?}"
);
}
let provider = FieldPath::Provider {
name: "nanogpt".to_string(),
field: "protocol",
};
assert_eq!(provider.to_string(), "providers.nanogpt.protocol");
assert_eq!(FieldPath::Project("name").to_string(), "project.name");
Ok(())
}
fn path_in(section: Section, field: &'static str) -> Option<FieldPath> {
Some(match section {
Section::Topic => FieldPath::Topic {
index: 0,
name: "ownership".to_string(),
field,
},
Section::Role => FieldPath::Role {
role: Role::Generator,
field,
},
Section::Pipeline => FieldPath::Pipeline(field),
Section::Training => FieldPath::Training(field),
Section::Export => FieldPath::Export(field),
Section::Hub => FieldPath::Hub(field),
Section::Metrics => FieldPath::Metrics(field),
Section::Target(TargetKind::Runpod) => FieldPath::Target {
name: "cloud".to_string(),
field,
},
Section::Project | Section::Provider | Section::Target(_) => return None,
})
}
fn probes(kind: FieldKind) -> Vec<FieldValue> {
match kind {
FieldKind::Int { min, max } => [
Some(min - 1),
Some(min),
Some(min + 1),
Some(max),
max.checked_add(1),
]
.into_iter()
.flatten()
.map(FieldValue::Int)
.collect(),
FieldKind::Float { min, max } => [min, max]
.into_iter()
.filter_map(|bound| match bound {
Bound::Incl(end) | Bound::Excl(end) => Some(end),
Bound::Unbounded => None,
})
.flat_map(|end| [end - 0.5, end, end + 0.5])
.map(FieldValue::Float)
.collect(),
_ => Vec::new(),
}
}
#[test]
fn numeric_bounds_match_validate() -> TestResult {
let mut doc = ConfigDoc::parse(COMMENTED)?;
doc.add_target("cloud", TargetKind::Runpod)?;
let gpus = FieldPath::Target {
name: "cloud".to_string(),
field: "gpu_types",
};
doc.set(&gpus, FieldValue::Text("auto".to_string()))?;
validate(&doc)?;
let sections = [
Section::Topic,
Section::Role,
Section::Pipeline,
Section::Training,
Section::Target(TargetKind::Runpod),
];
let mut probed = 0;
for section in sections {
for spec in fields::for_section(section) {
let Some(path) = path_in(section, spec.name) else {
continue;
};
for value in probes(spec.kind) {
let accepted = spec.kind.check(&value).is_ok();
let mut edited = doc.clone();
edited.raw_set(&path, value.clone())?;
let found = problems(&edited);
let refused = !found.is_empty();
let key = match &path {
FieldPath::Topic { index, field, .. } => format!("topics[{index}]{field}"),
_ => path.table(),
};
assert!(
found
.iter()
.all(|problem| problem.contains(&path.to_string())
|| problem.contains(&key)),
"{path} = {value:?}: {found:?}"
);
let needs_more = ["thinking_budget", "max_volume_gb"].contains(&spec.name);
if needs_more && !accepted {
assert!(refused, "{path} = {value:?} accepted: {found:?}");
} else if !needs_more {
assert_eq!(accepted, !refused, "{path} = {value:?}: {found:?}");
}
probed += 1;
}
}
}
assert!(probed > 100, "{probed}");
Ok(())
}
}