use std::fs;
use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use crate::paths::resolve_config_dir;
use crate::tool_registry::ToolManifest;
#[derive(Debug, Error)]
pub enum ToolSettingsError {
#[error("io error on {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("could not parse settings {path}: {source}")]
Parse {
path: PathBuf,
#[source]
source: serde_json::Error,
},
}
type Result<T> = std::result::Result<T, ToolSettingsError>;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct LaunchSettings {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub port: Option<u16>,
#[serde(default = "default_bind")]
pub bind: String,
#[serde(default = "default_true")]
pub autostart: bool,
}
fn default_true() -> bool {
true
}
fn default_bind() -> String {
"127.0.0.1".to_string()
}
impl Default for LaunchSettings {
fn default() -> Self {
LaunchSettings {
enabled: true,
port: None,
bind: default_bind(),
autostart: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ToolSettings {
#[serde(default)]
pub launch: LaunchSettings,
#[serde(default = "empty_object")]
pub extra: Value,
#[serde(skip)]
name: String,
}
fn empty_object() -> Value {
Value::Object(serde_json::Map::new())
}
impl Default for ToolSettings {
fn default() -> Self {
ToolSettings {
launch: LaunchSettings::default(),
extra: empty_object(),
name: String::new(),
}
}
}
pub fn settings_dir() -> PathBuf {
resolve_config_dir().join("tool-settings")
}
fn settings_path(name: &str) -> PathBuf {
settings_dir().join(format!("{name}.json"))
}
impl ToolSettings {
pub fn open(name: &str) -> Result<ToolSettings> {
let path = settings_path(name);
if !path.is_file() {
let mut s = ToolSettings::default();
s.name = name.to_string();
return Ok(s);
}
let text = fs::read_to_string(&path).map_err(|source| ToolSettingsError::Io {
path: path.clone(),
source,
})?;
let mut s: ToolSettings =
serde_json::from_str(&text).map_err(|source| ToolSettingsError::Parse {
path: path.clone(),
source,
})?;
s.name = name.to_string();
Ok(s)
}
pub fn open_seeded(manifest: &ToolManifest) -> Result<ToolSettings> {
let existed = settings_path(&manifest.name).is_file();
let mut s = ToolSettings::open(&manifest.name)?;
if !existed {
if s.launch.port.is_none() {
s.launch.port = manifest.launch.default_port;
}
s.launch.autostart = manifest.launch.autostart;
}
Ok(s)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn save(&self) -> Result<()> {
let dir = settings_dir();
fs::create_dir_all(&dir).map_err(|source| ToolSettingsError::Io {
path: dir.clone(),
source,
})?;
let dest = settings_path(&self.name);
let text =
serde_json::to_string_pretty(self).map_err(|source| ToolSettingsError::Parse {
path: dest.clone(),
source,
})?;
let tmp = dir.join(format!(".{}.tmp.{}", self.name, std::process::id()));
fs::write(&tmp, text.as_bytes()).map_err(|source| ToolSettingsError::Io {
path: tmp.clone(),
source,
})?;
fs::rename(&tmp, &dest).map_err(|source| ToolSettingsError::Io {
path: dest.clone(),
source,
})?;
Ok(())
}
}
pub fn effective_http_addr(settings: &ToolSettings) -> Option<String> {
settings
.launch
.port
.map(|p| format!("{}:{}", settings.launch.bind, p))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::paths::test_support::with_temp_config;
use crate::tool_registry::{LaunchMode, LaunchSpec};
fn manifest() -> ToolManifest {
ToolManifest {
schema_version: 1,
name: "labelit-studio".into(),
version: "1.1.3".into(),
description: None,
executable: "/opt/autocore/bin/modules/labelit-studio".into(),
serves_http: true,
ui_path: Some("/".into()),
launch: LaunchSpec {
mode: LaunchMode::Service,
default_port: Some(7878),
autostart: true,
args: vec![],
},
editors: vec![],
}
}
#[test]
fn defaults_are_safe() {
let s = ToolSettings::default();
assert!(s.launch.enabled);
assert_eq!(s.launch.bind, "127.0.0.1", "must default to loopback");
assert!(s.extra.is_object());
}
#[test]
fn open_seeded_fills_from_manifest_then_persists() {
with_temp_config("seed", || {
let mut s = ToolSettings::open_seeded(&manifest()).unwrap();
assert_eq!(s.launch.port, Some(7878));
s.launch.port = Some(7900); s.extra["last_image_dir"] = Value::String("/snaps".into());
s.save().unwrap();
let mut m = manifest();
m.launch.default_port = Some(1234);
let reopened = ToolSettings::open_seeded(&m).unwrap();
assert_eq!(reopened.launch.port, Some(7900));
assert_eq!(reopened.extra["last_image_dir"], "/snaps");
assert_eq!(reopened.launch.bind, "127.0.0.1");
});
}
#[test]
fn effective_addr() {
let mut s = ToolSettings::default();
assert!(effective_http_addr(&s).is_none());
s.launch.port = Some(7878);
assert_eq!(effective_http_addr(&s).as_deref(), Some("127.0.0.1:7878"));
}
}