- load config from json - cli commands to launch existing profiles - tui to pick custom directories and venv
187 lines
5.7 KiB
Rust
187 lines
5.7 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use anyhow::{Context, Result};
|
|
use serde::Deserialize;
|
|
|
|
/// A launcher entry as it appears in `defaults.json`, where `gui_file` and
|
|
/// `server_file` may be omitted (falling back to the top-level defaults).
|
|
#[derive(Debug, Deserialize)]
|
|
struct RawLauncherEntry {
|
|
/// Virtualenv directory, relative to `venvs_base`.
|
|
venv: PathBuf,
|
|
/// Source directory, relative to `apps_base`.
|
|
src: PathBuf,
|
|
/// GUI entry point, relative to the entry's `src` directory.
|
|
gui_file: Option<PathBuf>,
|
|
/// Server entry point, relative to the entry's `src` directory.
|
|
server_file: Option<PathBuf>,
|
|
}
|
|
|
|
/// The configuration as parsed directly from `defaults.json`.
|
|
#[derive(Debug, Deserialize)]
|
|
struct RawConfig {
|
|
apps_base: PathBuf,
|
|
venvs_base: PathBuf,
|
|
default_gui_file: PathBuf,
|
|
default_server_file: PathBuf,
|
|
map: HashMap<String, RawLauncherEntry>,
|
|
}
|
|
|
|
/// A fully-resolved launcher entry with all paths expanded to absolute paths.
|
|
#[derive(Debug, Clone)]
|
|
pub struct LauncherEntry {
|
|
/// Python interpreter inside the virtualenv.
|
|
pub python: PathBuf,
|
|
/// Absolute path to the GUI entry point.
|
|
pub gui_file: PathBuf,
|
|
/// Absolute path to the server entry point.
|
|
pub server_file: PathBuf,
|
|
}
|
|
|
|
/// The resolved launcher configuration.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Config {
|
|
/// Base directory containing the source/app directories.
|
|
pub apps_base: PathBuf,
|
|
/// Base directory containing the virtualenvs.
|
|
pub venvs_base: PathBuf,
|
|
/// Resolved launcher entries, keyed by beamline.
|
|
pub entries: HashMap<String, LauncherEntry>,
|
|
}
|
|
|
|
/// Join `rel` onto `base`, treating `rel` as relative even if it starts with a
|
|
/// leading separator (as the entry points in `defaults.json` could).
|
|
fn join_relative(base: &Path, rel: &Path) -> PathBuf {
|
|
base.join(rel.strip_prefix("/").unwrap_or(rel))
|
|
}
|
|
|
|
impl RawLauncherEntry {
|
|
/// Resolve this raw entry into absolute paths using the top-level bases and
|
|
/// defaults.
|
|
fn resolve(
|
|
self,
|
|
apps_base: &Path,
|
|
venvs_base: &Path,
|
|
default_gui_file: &Path,
|
|
default_server_file: &Path,
|
|
) -> LauncherEntry {
|
|
let src_dir = apps_base.join(&self.src);
|
|
let gui_file = self.gui_file.as_deref().unwrap_or(default_gui_file);
|
|
let server_file = self.server_file.as_deref().unwrap_or(default_server_file);
|
|
|
|
LauncherEntry {
|
|
python: venvs_base.join(&self.venv).join("bin/python"),
|
|
gui_file: join_relative(&src_dir, gui_file),
|
|
server_file: join_relative(&src_dir, server_file),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parse a `defaults.json` document into a resolved [`Config`].
|
|
pub fn load_config(json: &str) -> Result<Config> {
|
|
let deserializer = &mut serde_json::Deserializer::from_str(json);
|
|
let raw: RawConfig = serde_path_to_error::deserialize(deserializer)
|
|
.context("failed to parse config JSON")?;
|
|
|
|
let entries = raw
|
|
.map
|
|
.into_iter()
|
|
.map(|(key, entry)| {
|
|
let resolved = entry.resolve(
|
|
&raw.apps_base,
|
|
&raw.venvs_base,
|
|
&raw.default_gui_file,
|
|
&raw.default_server_file,
|
|
);
|
|
(key, resolved)
|
|
})
|
|
.collect();
|
|
|
|
Ok(Config {
|
|
apps_base: raw.apps_base,
|
|
venvs_base: raw.venvs_base,
|
|
entries,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const SAMPLE: &str = r#"{
|
|
"apps_base": "/apps",
|
|
"venvs_base": "/venvs",
|
|
"default_gui_file": "/src/gui.py",
|
|
"default_server_file": "/src/server.py",
|
|
"map": {
|
|
"x10sa": {
|
|
"venv": "pxii",
|
|
"src": "aaredaq-pxii",
|
|
"gui_file": "/custom/gui.py",
|
|
"server_file": "/custom/server.py"
|
|
},
|
|
"x06da": {
|
|
"venv": "pxiii",
|
|
"src": "aaredaq-pxiii"
|
|
}
|
|
}
|
|
}"#;
|
|
|
|
#[test]
|
|
fn expands_python_and_entry_points() {
|
|
let config = load_config(SAMPLE).unwrap();
|
|
|
|
let entry = &config.entries["x10sa"];
|
|
assert_eq!(entry.python, PathBuf::from("/venvs/pxii/bin/python"));
|
|
assert_eq!(entry.gui_file, PathBuf::from("/apps/aaredaq-pxii/custom/gui.py"));
|
|
assert_eq!(
|
|
entry.server_file,
|
|
PathBuf::from("/apps/aaredaq-pxii/custom/server.py")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn falls_back_to_defaults_when_entry_points_omitted() {
|
|
let config = load_config(SAMPLE).unwrap();
|
|
|
|
let entry = &config.entries["x06da"];
|
|
assert_eq!(entry.python, PathBuf::from("/venvs/pxiii/bin/python"));
|
|
assert_eq!(entry.gui_file, PathBuf::from("/apps/aaredaq-pxiii/src/gui.py"));
|
|
assert_eq!(
|
|
entry.server_file,
|
|
PathBuf::from("/apps/aaredaq-pxiii/src/server.py")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_all_entries() {
|
|
let config = load_config(SAMPLE).unwrap();
|
|
assert_eq!(config.entries.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_json_is_an_error() {
|
|
assert!(load_config("not json").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn error_reports_field_path() {
|
|
// `venv` has the wrong type in the single entry.
|
|
let bad = r#"{
|
|
"apps_base": "/apps",
|
|
"venvs_base": "/venvs",
|
|
"default_gui_file": "/src/gui.py",
|
|
"default_server_file": "/src/server.py",
|
|
"map": { "x10sa": { "venv": 123, "src": "aaredaq-pxii" } }
|
|
}"#;
|
|
|
|
let err = load_config(bad).unwrap_err();
|
|
let message = format!("{err:#}");
|
|
assert!(
|
|
message.contains("map.x10sa.venv"),
|
|
"expected field path in error, got: {message}"
|
|
);
|
|
}
|
|
}
|