use clap::{Args, Subcommand};
use serde::Serialize;
use serde_json::Value;
use homeboy::defaults::{self, Defaults, HomeboyConfig};
use super::CmdResult;
#[derive(Args)]
pub struct ConfigArgs {
#[command(subcommand)]
command: ConfigCommand,
}
#[derive(Subcommand)]
enum ConfigCommand {
Show {
#[arg(long)]
builtin: bool,
},
Set {
pointer: String,
value: String,
},
Remove {
pointer: String,
},
Reset,
Path,
}
#[derive(Debug, Serialize)]
pub struct ConfigOutput {
command: String,
#[serde(skip_serializing_if = "Option::is_none")]
config: Option<HomeboyConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
defaults: Option<Defaults>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
exists: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pointer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
value: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
deleted: Option<bool>,
}
pub fn run(args: ConfigArgs, _global: &crate::commands::GlobalArgs) -> CmdResult<ConfigOutput> {
match args.command {
ConfigCommand::Show { builtin } => show(builtin),
ConfigCommand::Set { pointer, value } => set(&pointer, &value),
ConfigCommand::Remove { pointer } => remove(&pointer),
ConfigCommand::Reset => reset(),
ConfigCommand::Path => path(),
}
}
fn show(builtin: bool) -> CmdResult<ConfigOutput> {
if builtin {
Ok((
ConfigOutput {
command: "config.show".to_string(),
defaults: Some(defaults::builtin_defaults()),
config: None,
path: None,
exists: None,
pointer: None,
value: None,
deleted: None,
},
0,
))
} else {
let config = defaults::load_config();
Ok((
ConfigOutput {
command: "config.show".to_string(),
config: Some(config),
defaults: None,
path: None,
exists: None,
pointer: None,
value: None,
deleted: None,
},
0,
))
}
}
fn set(pointer: &str, value_str: &str) -> CmdResult<ConfigOutput> {
if !pointer.starts_with('/') {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
"JSON pointer must start with '/'",
None,
None,
));
}
let value: Value = serde_json::from_str(value_str).map_err(|e| {
homeboy::Error::validation_invalid_json(
e,
Some("parse value".to_string()),
Some(value_str.chars().take(200).collect::<String>()),
)
})?;
let mut config = defaults::load_config();
let mut config_json = serde_json::to_value(&config).map_err(|e| {
homeboy::Error::internal_unexpected(format!("Failed to serialize config: {}", e))
})?;
set_json_pointer(&mut config_json, pointer, value.clone())?;
config = serde_json::from_value(config_json).map_err(|e| {
homeboy::Error::validation_invalid_json(e, Some("deserialize config".to_string()), None)
})?;
defaults::save_config(&config)?;
Ok((
ConfigOutput {
command: "config.set".to_string(),
config: Some(config),
defaults: None,
path: None,
exists: None,
pointer: Some(pointer.to_string()),
value: Some(value),
deleted: None,
},
0,
))
}
fn remove(pointer: &str) -> CmdResult<ConfigOutput> {
if !pointer.starts_with('/') {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
"JSON pointer must start with '/'",
None,
None,
));
}
let mut config = defaults::load_config();
let mut config_json = serde_json::to_value(&config).map_err(|e| {
homeboy::Error::internal_unexpected(format!("Failed to serialize config: {}", e))
})?;
remove_json_pointer(&mut config_json, pointer)?;
config = serde_json::from_value(config_json).map_err(|e| {
homeboy::Error::validation_invalid_json(e, Some("deserialize config".to_string()), None)
})?;
defaults::save_config(&config)?;
Ok((
ConfigOutput {
command: "config.remove".to_string(),
config: Some(config),
defaults: None,
path: None,
exists: None,
pointer: Some(pointer.to_string()),
value: None,
deleted: None,
},
0,
))
}
fn reset() -> CmdResult<ConfigOutput> {
let deleted = defaults::reset_config()?;
Ok((
ConfigOutput {
command: "config.reset".to_string(),
config: None,
defaults: Some(defaults::builtin_defaults()),
path: Some(defaults::config_path()?),
exists: None,
pointer: None,
value: None,
deleted: Some(deleted),
},
0,
))
}
fn path() -> CmdResult<ConfigOutput> {
let path = defaults::config_path()?;
let exists = defaults::config_exists();
Ok((
ConfigOutput {
command: "config.path".to_string(),
config: None,
defaults: None,
path: Some(path),
exists: Some(exists),
pointer: None,
value: None,
deleted: None,
},
0,
))
}
fn set_json_pointer(root: &mut Value, pointer: &str, value: Value) -> homeboy::Result<()> {
let parts: Vec<&str> = pointer[1..].split('/').collect();
if parts.is_empty() {
*root = value;
return Ok(());
}
let mut current = root;
for (i, part) in parts.iter().enumerate() {
let key = unescape_json_pointer(part);
if i == parts.len() - 1 {
match current {
Value::Object(map) => {
map.insert(key, value);
return Ok(());
}
Value::Array(arr) => {
let index: usize = key.parse().map_err(|_| {
homeboy::Error::validation_invalid_argument(
"pointer",
format!("Invalid array index: {}", key),
None,
None,
)
})?;
if index < arr.len() {
arr[index] = value;
} else if index == arr.len() {
arr.push(value);
} else {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
format!("Array index {} out of bounds (length {})", index, arr.len()),
None,
None,
));
}
return Ok(());
}
_ => {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
format!("Cannot set property on non-object at path: {}", pointer),
None,
None,
));
}
}
} else {
match current {
Value::Object(map) => {
if !map.contains_key(&key) {
map.insert(key.clone(), Value::Object(serde_json::Map::new()));
}
current = map.get_mut(&key).unwrap();
}
Value::Array(arr) => {
let index: usize = key.parse().map_err(|_| {
homeboy::Error::validation_invalid_argument(
"pointer",
format!("Invalid array index: {}", key),
None,
None,
)
})?;
if index >= arr.len() {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
format!("Array index {} out of bounds (length {})", index, arr.len()),
None,
None,
));
}
current = &mut arr[index];
}
_ => {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
format!("Cannot navigate through non-object at path: {}", pointer),
None,
None,
));
}
}
}
}
Ok(())
}
fn remove_json_pointer(root: &mut Value, pointer: &str) -> homeboy::Result<()> {
let parts: Vec<&str> = pointer[1..].split('/').collect();
if parts.is_empty() {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
"Cannot remove root element",
None,
None,
));
}
let mut current = root;
for (i, part) in parts.iter().enumerate() {
let key = unescape_json_pointer(part);
if i == parts.len() - 1 {
match current {
Value::Object(map) => {
if map.remove(&key).is_none() {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
format!("Key '{}' not found", key),
None,
None,
));
}
return Ok(());
}
Value::Array(arr) => {
let index: usize = key.parse().map_err(|_| {
homeboy::Error::validation_invalid_argument(
"pointer",
format!("Invalid array index: {}", key),
None,
None,
)
})?;
if index >= arr.len() {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
format!("Array index {} out of bounds (length {})", index, arr.len()),
None,
None,
));
}
arr.remove(index);
return Ok(());
}
_ => {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
format!("Cannot remove from non-object at path: {}", pointer),
None,
None,
));
}
}
} else {
match current {
Value::Object(map) => {
current = map.get_mut(&key).ok_or_else(|| {
homeboy::Error::validation_invalid_argument(
"pointer",
format!("Key '{}' not found", key),
None,
None,
)
})?;
}
Value::Array(arr) => {
let index: usize = key.parse().map_err(|_| {
homeboy::Error::validation_invalid_argument(
"pointer",
format!("Invalid array index: {}", key),
None,
None,
)
})?;
if index >= arr.len() {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
format!("Array index {} out of bounds (length {})", index, arr.len()),
None,
None,
));
}
current = &mut arr[index];
}
_ => {
return Err(homeboy::Error::validation_invalid_argument(
"pointer",
format!("Cannot navigate through non-object at path: {}", pointer),
None,
None,
));
}
}
}
}
Ok(())
}
fn unescape_json_pointer(s: &str) -> String {
s.replace("~1", "/").replace("~0", "~")
}