use std::fs;
use std::path::PathBuf;
use core_api::{SnapshotIncludeFlags, SnapshotRateLimit, SnapshotConfig};
use crate::helpers::{has_flag, option_value, parse_u64_option};
use crate::gateway::{
STATUS_OP_SNAPSHOT_CONFIG_GET, STATUS_OP_SNAPSHOT_CONFIG_SET, STATUS_SERVICE_NAME,
StatusServiceResponse, build_config_request, build_request, make_udp_service_client,
next_request_id, normalize_udp_endpoint, validate_response,
};
const DEFAULT_CONFIG_PATH: &str = "artifacts/introspection/snapshot-config.json";
const SNAPSHOT_CONFIG_FIELDS: [(&str, &str, &str); 13] = [
("enabled", "bool", "Enable periodic snapshot emission."),
(
"status_report_path",
"path",
"Output path for StatusSnapshot JSON report.",
),
(
"runtime_report_path",
"path",
"Output path for RuntimeLoadReport JSON report.",
),
(
"middleware_report_path",
"path",
"Output path for MiddlewareLoadReport JSON report.",
),
(
"resource_report_path",
"path",
"Output path for ResourceCatalogReport JSON report.",
),
(
"interval_ms",
"u64",
"Minimum interval between periodic snapshot writes in milliseconds.",
),
(
"rate_limit.max_writes_per_sec",
"u32",
"Token refill rate for write limiting; 0 means unlimited.",
),
(
"rate_limit.burst",
"u32",
"Maximum token burst size for immediate writes.",
),
("atomic_write", "bool", "Write snapshots atomically via temp file + rename."),
(
"include_flags.status",
"bool",
"Emit status snapshot file when true.",
),
(
"include_flags.runtime",
"bool",
"Emit runtime report file when true.",
),
(
"include_flags.middleware",
"bool",
"Emit middleware report file when true.",
),
(
"include_flags.resource",
"bool",
"Emit resource catalog file when true.",
),
];
#[derive(Clone, Copy)]
enum SnapshotConfigSource<'a> {
Local {
path: &'a PathBuf,
saved: bool,
},
Remote {
endpoint: &'a str,
applied: bool,
},
}
pub fn snapshot_config(args: &[String]) -> Result<(), String> {
let (args, list_subcommand) = normalize_args(args);
if let Some(raw_endpoint) = option_value(args, "--endpoint") {
let endpoint = normalize_udp_endpoint(&raw_endpoint)?;
return snapshot_config_remote(args, &endpoint, list_subcommand);
}
let config_path = option_value(args, "--file")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_PATH));
let has_overrides = has_config_overrides(args);
let json = has_flag(args, "--json");
let list_fields = list_subcommand || has_flag(args, "--list") || has_flag(args, "--list-fields");
let field = option_value(args, "--field");
let no_save = has_flag(args, "--no-save");
let mut config = if config_path.exists() {
let raw = fs::read_to_string(&config_path)
.map_err(|err| format!("read config {} failed: {err}", config_path.display()))?;
serde_json::from_str::<SnapshotConfig>(&raw).map_err(|err| {
format!(
"parse config {} as SnapshotConfig failed: {err}",
config_path.display()
)
})?
} else {
SnapshotConfig::default()
};
if has_overrides {
apply_overrides(args, &mut config)?;
}
let saved = !no_save && (has_overrides || !config_path.exists());
if saved {
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).map_err(|err| {
format!(
"create config parent dir {} failed: {err}",
parent.display()
)
})?;
}
let body = serde_json::to_string_pretty(&config)
.map_err(|err| format!("serialize snapshot config failed: {err}"))?;
fs::write(&config_path, body).map_err(|err| {
format!(
"write snapshot config to {} failed: {err}",
config_path.display()
)
})?;
}
print_config_output(
&config,
SnapshotConfigSource::Local {
path: &config_path,
saved,
},
json,
list_fields,
field,
)
}
fn snapshot_config_remote(
args: &[String],
endpoint: &str,
list_subcommand: bool,
) -> Result<(), String> {
let timeout_ms = parse_u64_option(args, "--timeout-ms", 1000)?;
let json = has_flag(args, "--json");
let list_fields = list_subcommand || has_flag(args, "--list") || has_flag(args, "--list-fields");
let field = option_value(args, "--field");
let client = make_udp_service_client(endpoint.to_string(), timeout_ms)?;
let get_request_id = next_request_id();
let get_request = build_request(STATUS_OP_SNAPSHOT_CONFIG_GET);
let get_response: StatusServiceResponse = client
.call_json(STATUS_SERVICE_NAME, get_request_id, &get_request)
.map_err(|err| format!("snapshot config query to {endpoint} failed: {err}"))?;
validate_response(
&get_response,
get_request_id,
STATUS_OP_SNAPSHOT_CONFIG_GET,
)?;
let mut config = get_response.snapshot_config.ok_or_else(|| {
format!("snapshot config response from {endpoint} missing config payload")
})?;
let applied = has_config_overrides(args);
if applied {
apply_overrides(args, &mut config)?;
}
if applied {
let set_request_id = next_request_id();
let set_request = build_config_request(STATUS_OP_SNAPSHOT_CONFIG_SET, config.clone());
let set_response: StatusServiceResponse = client
.call_json(STATUS_SERVICE_NAME, set_request_id, &set_request)
.map_err(|err| format!("snapshot config apply to {endpoint} failed: {err}"))?;
validate_response(
&set_response,
set_request_id,
STATUS_OP_SNAPSHOT_CONFIG_SET,
)?;
config = set_response.snapshot_config.ok_or_else(|| {
format!("snapshot config set response from {endpoint} missing config payload")
})?;
}
print_config_output(
&config,
SnapshotConfigSource::Remote { endpoint, applied },
json,
list_fields,
field,
)
}
fn normalize_args(args: &[String]) -> (&[String], bool) {
if let Some(sub) = args.first().map(String::as_str)
&& matches!(sub, "list" | "fields")
{
return (&args[1..], true);
}
(args, false)
}
fn print_config_output(
config: &SnapshotConfig,
source: SnapshotConfigSource<'_>,
json: bool,
list_fields: bool,
field: Option<String>,
) -> Result<(), String> {
if let Some(key) = field {
return print_single_field(config, source, &key, json);
}
if list_fields {
return print_field_list(config, source, json);
}
if json {
let payload = serde_json::json!({
"api_version": "robotrt.snapshot.config.v1",
"source": source_json(source),
"config": config,
});
println!(
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("serialize snapshot config json failed: {err}"))?
);
return Ok(());
}
println!("RobotRT Snapshot Config");
print_source_human(source);
println!("enabled: {}", config.enabled);
println!("interval_ms: {}", config.interval_ms);
println!(
"rate_limit: max_writes_per_sec={} burst={}",
config.rate_limit.max_writes_per_sec, config.rate_limit.burst
);
println!("atomic_write: {}", config.atomic_write);
println!(
"include_flags: status={} runtime={} middleware={} resource={}",
config.include_flags.status,
config.include_flags.runtime,
config.include_flags.middleware,
config.include_flags.resource,
);
println!("status_report: {}", config.status_report_path);
println!("runtime_report: {}", config.runtime_report_path);
println!("middleware_report: {}", config.middleware_report_path);
println!("resource_report: {}", config.resource_report_path);
Ok(())
}
fn print_field_list(
config: &SnapshotConfig,
source: SnapshotConfigSource<'_>,
json: bool,
) -> Result<(), String> {
let fields = SNAPSHOT_CONFIG_FIELDS
.iter()
.map(|(key, value_type, desc)| {
serde_json::json!({
"key": key,
"type": value_type,
"description": desc,
"value": config_field_value(config, key),
})
})
.collect::<Vec<_>>();
if json {
let payload = serde_json::json!({
"api_version": "robotrt.snapshot.config.fields.v1",
"source": source_json(source),
"fields": fields,
});
println!(
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("serialize snapshot config fields json failed: {err}"))?
);
return Ok(());
}
println!("RobotRT Snapshot Config Fields");
print_source_human(source);
for (key, value_type, desc) in SNAPSHOT_CONFIG_FIELDS {
let value = config_field_value(config, key);
let rendered = serde_json::to_string(&value)
.map_err(|err| format!("serialize field {key} value failed: {err}"))?;
println!("- {key} ({value_type}): {desc}");
println!(" value: {rendered}");
}
Ok(())
}
fn print_single_field(
config: &SnapshotConfig,
source: SnapshotConfigSource<'_>,
key: &str,
json: bool,
) -> Result<(), String> {
let normalized = normalize_field_key(key);
let value = match find_field_value(config, normalized.as_str()) {
Some(value) => value,
None => {
let known = SNAPSHOT_CONFIG_FIELDS
.iter()
.map(|(name, _, _)| *name)
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"unknown --field key: {key}. known fields: {known}"
));
}
};
if json {
let payload = serde_json::json!({
"api_version": "robotrt.snapshot.config.field.v1",
"source": source_json(source),
"field": {
"key": normalized,
"value": value,
}
});
println!(
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|err| format!("serialize snapshot config field json failed: {err}"))?
);
return Ok(());
}
println!("RobotRT Snapshot Config Field");
print_source_human(source);
println!("key: {normalized}");
println!(
"value: {}",
serde_json::to_string(&value)
.map_err(|err| format!("serialize snapshot config field value failed: {err}"))?
);
Ok(())
}
fn source_json(source: SnapshotConfigSource<'_>) -> serde_json::Value {
match source {
SnapshotConfigSource::Local { path, saved } => serde_json::json!({
"mode": "file",
"path": path,
"saved": saved,
}),
SnapshotConfigSource::Remote { endpoint, applied } => serde_json::json!({
"mode": "remote_service",
"endpoint": endpoint,
"applied": applied,
}),
}
}
fn print_source_human(source: SnapshotConfigSource<'_>) {
match source {
SnapshotConfigSource::Local { path, saved } => {
println!("mode: file");
println!("path: {}", path.display());
println!("saved: {saved}");
}
SnapshotConfigSource::Remote { endpoint, applied } => {
println!("mode: remote_service");
println!("endpoint: {endpoint}");
println!("applied: {applied}");
}
}
}
fn config_field_value(config: &SnapshotConfig, key: &str) -> serde_json::Value {
find_field_value(config, key).unwrap_or(serde_json::Value::Null)
}
fn find_field_value(config: &SnapshotConfig, key: &str) -> Option<serde_json::Value> {
match key {
"enabled" => Some(serde_json::json!(config.enabled)),
"status_report_path" => Some(serde_json::json!(config.status_report_path)),
"runtime_report_path" => Some(serde_json::json!(config.runtime_report_path)),
"middleware_report_path" => Some(serde_json::json!(config.middleware_report_path)),
"resource_report_path" => Some(serde_json::json!(config.resource_report_path)),
"interval_ms" => Some(serde_json::json!(config.interval_ms)),
"rate_limit.max_writes_per_sec" => {
Some(serde_json::json!(config.rate_limit.max_writes_per_sec))
}
"rate_limit.burst" => Some(serde_json::json!(config.rate_limit.burst)),
"atomic_write" => Some(serde_json::json!(config.atomic_write)),
"include_flags.status" => Some(serde_json::json!(config.include_flags.status)),
"include_flags.runtime" => Some(serde_json::json!(config.include_flags.runtime)),
"include_flags.middleware" => Some(serde_json::json!(config.include_flags.middleware)),
"include_flags.resource" => Some(serde_json::json!(config.include_flags.resource)),
_ => None,
}
}
fn normalize_field_key(raw: &str) -> String {
match raw {
"status_report" => String::from("status_report_path"),
"runtime_report" => String::from("runtime_report_path"),
"middleware_report" => String::from("middleware_report_path"),
"resource_report" => String::from("resource_report_path"),
"rate_max" => String::from("rate_limit.max_writes_per_sec"),
"rate_burst" => String::from("rate_limit.burst"),
other => other.to_string(),
}
}
fn apply_overrides(args: &[String], config: &mut SnapshotConfig) -> Result<(), String> {
if has_flag(args, "--enable") {
config.enabled = true;
}
if has_flag(args, "--disable") {
config.enabled = false;
}
if let Some(raw) = option_value(args, "--enabled") {
config.enabled = parse_bool(&raw, "--enabled")?;
}
if option_value(args, "--interval-ms").is_some() {
config.interval_ms = parse_u64_option(args, "--interval-ms", config.interval_ms)?;
}
if option_value(args, "--rate-max").is_some() {
config.rate_limit.max_writes_per_sec =
parse_u64_option(args, "--rate-max", config.rate_limit.max_writes_per_sec as u64)?
as u32;
}
if option_value(args, "--rate-burst").is_some() {
config.rate_limit.burst =
parse_u64_option(args, "--rate-burst", config.rate_limit.burst as u64)? as u32;
}
if let Some(raw) = option_value(args, "--atomic-write") {
config.atomic_write = parse_bool(&raw, "--atomic-write")?;
}
if let Some(raw) = option_value(args, "--include") {
config.include_flags = parse_include_flags(&raw)?;
}
if let Some(path) = option_value(args, "--status-report") {
config.status_report_path = path;
}
if let Some(path) = option_value(args, "--runtime-report") {
config.runtime_report_path = path;
}
if let Some(path) = option_value(args, "--middleware-report") {
config.middleware_report_path = path;
}
if let Some(path) = option_value(args, "--resource-report") {
config.resource_report_path = path;
}
if config.rate_limit.max_writes_per_sec > 0 && config.rate_limit.burst == 0 {
config.rate_limit = SnapshotRateLimit {
max_writes_per_sec: config.rate_limit.max_writes_per_sec,
burst: 1,
};
}
Ok(())
}
fn parse_bool(raw: &str, option: &str) -> Result<bool, String> {
match raw {
"1" | "true" | "yes" | "on" => Ok(true),
"0" | "false" | "no" | "off" => Ok(false),
_ => Err(format!(
"invalid value for {option}: {raw} (expected true|false|1|0|yes|no|on|off)"
)),
}
}
fn parse_include_flags(raw: &str) -> Result<SnapshotIncludeFlags, String> {
let mut flags = SnapshotIncludeFlags {
status: false,
runtime: false,
middleware: false,
resource: false,
};
for token in raw.split(',').map(str::trim).filter(|item| !item.is_empty()) {
match token {
"all" => {
flags = SnapshotIncludeFlags {
status: true,
runtime: true,
middleware: true,
resource: true,
};
}
"status" => flags.status = true,
"runtime" => flags.runtime = true,
"middleware" => flags.middleware = true,
"resource" => flags.resource = true,
other => {
return Err(format!(
"invalid --include item: {other} (expected status|runtime|middleware|resource|all)"
))
}
}
}
Ok(flags)
}
fn has_config_overrides(args: &[String]) -> bool {
has_flag(args, "--enable")
|| has_flag(args, "--disable")
|| option_value(args, "--enabled").is_some()
|| option_value(args, "--interval-ms").is_some()
|| option_value(args, "--rate-max").is_some()
|| option_value(args, "--rate-burst").is_some()
|| option_value(args, "--atomic-write").is_some()
|| option_value(args, "--include").is_some()
|| option_value(args, "--status-report").is_some()
|| option_value(args, "--runtime-report").is_some()
|| option_value(args, "--middleware-report").is_some()
|| option_value(args, "--resource-report").is_some()
}