use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::config::{project_dirs_from, resolve_or_self, Config};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PinClass {
Sovereign,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PinScope {
User,
Workspace,
Repo,
Session,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pin {
pub class: PinClass,
pub scope: PinScope,
pub source: Option<PathBuf>,
pub problem: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InferencePolicy {
pins: Vec<Pin>,
}
#[derive(Deserialize)]
struct InferenceTable {
inference: Option<InferenceSection>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct InferenceSection {
pin: Option<PinClass>,
}
impl InferencePolicy {
#[must_use]
pub fn unpinned() -> Self {
Self::default()
}
#[must_use]
pub fn is_pinned(&self) -> bool {
!self.pins.is_empty()
}
#[must_use]
pub fn pins(&self) -> &[Pin] {
&self.pins
}
#[must_use]
pub fn pin_session(mut self) -> Self {
self.pins.push(Pin {
class: PinClass::Sovereign,
scope: PinScope::Session,
source: None,
problem: None,
});
self
}
#[must_use]
pub fn meet(mut self, other: Self) -> Self {
self.pins.extend(other.pins);
self
}
pub fn discover(
cwd: std::io::Result<&Path>,
home: Option<&Path>,
user_config: Option<&Path>,
read: &dyn Fn(&Path) -> std::io::Result<String>,
resolve: &dyn Fn(&Path) -> PathBuf,
) -> Self {
let mut policy = Self::unpinned();
if let Some(path) = user_config {
policy.read_scope(path, PinScope::User, read);
}
let cwd = match cwd {
Ok(cwd) => cwd,
Err(e) => {
policy.pins.push(Pin {
class: PinClass::Sovereign,
scope: PinScope::Repo,
source: None,
problem: Some(format!("working directory unknown: {e}")),
});
return policy;
}
};
let mut repo_seen = false;
for dir in project_dirs_from(cwd, home, resolve) {
let path = dir.join(".newt").join("config.toml");
let scope = if repo_seen {
PinScope::Workspace
} else {
PinScope::Repo
};
if policy.read_scope(&path, scope, read) {
repo_seen = true;
}
}
policy
}
#[must_use]
pub fn discover_here() -> Self {
let home = crate::config::home_dir();
let user = Config::user_config_path();
let cwd = std::env::current_dir();
Self::discover(
cwd.as_deref()
.map_err(|e| std::io::Error::new(e.kind(), e.to_string())),
home.as_deref(),
user.as_deref(),
&|p| std::fs::read_to_string(p),
&resolve_or_self,
)
}
fn read_scope(
&mut self,
path: &Path,
scope: PinScope,
read: &dyn Fn(&Path) -> std::io::Result<String>,
) -> bool {
let problem = match read(path) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return false,
Err(e) => Some(format!("unreadable: {e}")),
Ok(text) => match toml::from_str::<InferenceTable>(&text) {
Ok(InferenceTable {
inference: Some(InferenceSection { pin: Some(class) }),
}) => {
self.push(class, scope, path, None);
return true;
}
Ok(_) => return true,
Err(e) => Some(format!("cannot read [inference]: {}", e.message())),
},
};
self.push(PinClass::Sovereign, scope, path, problem);
true
}
fn push(&mut self, class: PinClass, scope: PinScope, path: &Path, problem: Option<String>) {
self.pins.push(Pin {
class,
scope,
source: Some(path.to_path_buf()),
problem,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn reader(files: &[(&str, &str)]) -> impl Fn(&Path) -> std::io::Result<String> {
let map: HashMap<PathBuf, String> = files
.iter()
.map(|(p, t)| (PathBuf::from(p), (*t).to_owned()))
.collect();
move |p| {
map.get(p)
.cloned()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))
}
}
fn discover(files: &[(&str, &str)]) -> InferencePolicy {
InferencePolicy::discover(
Ok(Path::new("/w/org/repo")),
Some(Path::new("/home/op")),
Some(Path::new("/home/op/.newt/config.toml")),
&reader(files),
&same,
)
}
fn same(p: &Path) -> PathBuf {
p.to_path_buf()
}
const PINNED: &str = "[inference]\npin = \"sovereign\"\n";
#[test]
fn no_pin_anywhere_is_unpinned() {
let p = discover(&[("/w/org/repo/.newt/config.toml", "[context]\n")]);
assert!(!p.is_pinned());
}
#[test]
fn a_repo_pin_is_found_with_its_source() {
let p = discover(&[("/w/org/repo/.newt/config.toml", PINNED)]);
assert_eq!(p.pins().len(), 1);
assert_eq!(p.pins()[0].scope, PinScope::Repo);
assert_eq!(
p.pins()[0].source.as_deref(),
Some(Path::new("/w/org/repo/.newt/config.toml"))
);
}
#[test]
fn an_inner_unpinned_repo_stays_pinned_by_its_workspace() {
let p = discover(&[
("/w/org/repo/.newt/config.toml", "[context]\n"),
("/w/org/.newt/config.toml", PINNED),
]);
assert!(p.is_pinned());
assert_eq!(p.pins()[0].scope, PinScope::Workspace);
}
#[test]
fn pins_from_every_scope_accumulate() {
let p = discover(&[
("/home/op/.newt/config.toml", PINNED),
("/w/org/.newt/config.toml", PINNED),
("/w/org/repo/.newt/config.toml", PINNED),
]);
let scopes: Vec<_> = p.pins().iter().map(|p| p.scope.clone()).collect();
assert_eq!(
scopes,
[PinScope::User, PinScope::Repo, PinScope::Workspace]
);
}
#[test]
fn an_unknown_pin_class_fails_closed() {
for value in ["none", "Sovereign2", "false"] {
let text = format!("[inference]\npin = \"{value}\"\n");
let p = discover(&[("/w/org/repo/.newt/config.toml", &text)]);
assert!(p.is_pinned(), "{value}");
assert!(p.pins()[0].problem.is_some(), "{value}");
}
}
#[test]
fn an_unreadable_or_unparseable_config_fails_closed() {
let p = discover(&[("/w/org/repo/.newt/config.toml", "[inference\n")]);
assert!(p.is_pinned());
let denied = InferencePolicy::discover(
Ok(Path::new("/w/org/repo")),
Some(Path::new("/home/op")),
None,
&|_| Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
&same,
);
assert!(denied.is_pinned());
}
#[test]
fn the_home_dir_is_not_a_workspace() {
let p = InferencePolicy::discover(
Ok(Path::new("/home/op/src/repo")),
Some(Path::new("/home/op")),
Some(Path::new("/home/op/.newt/config.toml")),
&reader(&[("/home/op/.newt/config.toml", PINNED)]),
&same,
);
assert_eq!(p.pins().len(), 1);
assert_eq!(p.pins()[0].scope, PinScope::User);
}
#[test]
fn session_pin_and_meet_only_tighten() {
let p = InferencePolicy::unpinned().pin_session();
assert!(p.is_pinned());
let met = InferencePolicy::unpinned().meet(p.clone());
assert_eq!(met, p);
}
#[test]
fn inference_is_not_a_control_plane_key() {
assert!(!crate::config::CONTROL_PLANE_KEYS.contains(&"inference"));
}
#[test]
fn a_misspelled_inference_key_fails_closed() {
for text in [
"[inference]\npinn = \"sovereign\"\n",
"[inference]\nsovereign = true\n",
"[inference]\nPIN = \"sovereign\"\n",
] {
let p = discover(&[("/w/org/repo/.newt/config.toml", text)]);
assert!(p.is_pinned(), "{text}");
assert!(p.pins()[0].problem.is_some(), "{text}");
}
}
#[test]
fn an_unknown_working_directory_fails_closed() {
let p = InferencePolicy::discover(
Err(std::io::Error::from(std::io::ErrorKind::NotFound)),
Some(Path::new("/home/op")),
None,
&reader(&[]),
&same,
);
assert!(p.is_pinned());
let problem = p.pins()[0].problem.as_deref().unwrap();
assert!(problem.contains("working directory"), "{problem}");
}
}