fixed arrary logic
All checks were successful
Version Check / check-version (pull_request) Successful in 4s

This commit is contained in:
2026-03-18 17:35:07 +01:00
parent 277d8aa151
commit 30fd2d5d75
7 changed files with 198 additions and 187 deletions

View File

@@ -1,4 +1,4 @@
use crate::format::ConfigItem; use crate::format::{ConfigItem, PathSegment};
use tui_input::Input; use tui_input::Input;
/// Represents the current operating mode of the application. /// Represents the current operating mode of the application.
@@ -195,21 +195,17 @@ impl App {
let selected_path = self.vars[self.selected].path.clone(); let selected_path = self.vars[self.selected].path.clone();
let is_group = self.vars[self.selected].is_group; let is_group = self.vars[self.selected].is_group;
// Identify if the item being removed is an array item
let array_info = parse_index(&selected_path);
// 1. Identify all items to remove // 1. Identify all items to remove
let mut to_remove = Vec::new(); let mut to_remove = Vec::new();
to_remove.push(self.selected); to_remove.push(self.selected);
if is_group { if is_group {
let prefix_dot = format!("{}.", selected_path);
let prefix_bracket = format!("{}[", selected_path);
for (i, var) in self.vars.iter().enumerate() { for (i, var) in self.vars.iter().enumerate() {
if i == self.selected { if i == self.selected {
continue; continue;
} }
if var.path.starts_with(&prefix_dot) || var.path.starts_with(&prefix_bracket) { // An item is a child if its path starts with the selected path
if var.path.starts_with(&selected_path) {
to_remove.push(i); to_remove.push(i);
} }
} }
@@ -222,17 +218,19 @@ impl App {
} }
// 3. Re-index subsequent array items if applicable // 3. Re-index subsequent array items if applicable
if let Some((base, removed_idx)) = array_info { if let Some(PathSegment::Index(removed_idx)) = selected_path.last() {
let base = base.to_string(); let base_path = &selected_path[..selected_path.len() - 1];
for var in self.vars.iter_mut() { for var in self.vars.iter_mut() {
if var.path.starts_with(&base) { if var.path.starts_with(base_path) && var.path.len() >= selected_path.len() {
// We need to find the index segment that matches this array // Check if the element at the level of the removed index is an index
if let Some((b, i, suffix)) = find_array_segment(&var.path, &base) if let PathSegment::Index(i) = var.path[selected_path.len() - 1]
&& b == base && i > removed_idx { && i > *removed_idx {
let new_idx = i - 1; let new_idx = i - 1;
var.path = format!("{}[{}]{}", base, new_idx, suffix); var.path[selected_path.len() - 1] = PathSegment::Index(new_idx);
// Also update key if it matches the old index exactly
if var.key == format!("[{}]", i) { // If this was an array element itself (not a child property), update its key
if var.path.len() == selected_path.len() {
var.key = format!("[{}]", new_idx); var.key = format!("[{}]", new_idx);
} }
} }
@@ -254,14 +252,15 @@ impl App {
} }
self.save_undo_state(); self.save_undo_state();
let (base, idx, depth) = { let (base_path, idx, depth) = {
let selected_item = &self.vars[self.selected]; let selected_item = &self.vars[self.selected];
if selected_item.is_group { if selected_item.is_group {
return; return;
} }
let path = &selected_item.path; let path = &selected_item.path;
if let Some((base, idx)) = parse_index(path) {
(base.to_string(), idx, selected_item.depth) if let Some(PathSegment::Index(idx)) = path.last() {
(path[..path.len() - 1].to_vec(), *idx, selected_item.depth)
} else { } else {
return; return;
} }
@@ -276,21 +275,23 @@ impl App {
// 1. Shift all items in this array that have index >= new_idx // 1. Shift all items in this array that have index >= new_idx
for var in self.vars.iter_mut() { for var in self.vars.iter_mut() {
if var.path.starts_with(&base) if var.path.starts_with(&base_path) && var.path.len() > base_path.len()
&& let Some((b, i)) = parse_index(&var.path) && let PathSegment::Index(i) = var.path[base_path.len()]
&& b == base && i >= new_idx { && i >= new_idx {
var.path = format!("{}[{}]", base, i + 1); var.path[base_path.len()] = PathSegment::Index(i + 1);
// Also update key if it was just the index if var.path.len() == base_path.len() + 1 {
if var.key == format!("[{}]", i) {
var.key = format!("[{}]", i + 1); var.key = format!("[{}]", i + 1);
} }
} }
} }
// 2. Insert new item // 2. Insert new item
let mut new_path = base_path;
new_path.push(PathSegment::Index(new_idx));
let new_item = ConfigItem { let new_item = ConfigItem {
key: format!("[{}]", new_idx), key: format!("[{}]", new_idx),
path: format!("{}[{}]", base, new_idx), path: new_path,
value: Some("".to_string()), value: Some("".to_string()),
template_value: None, template_value: None,
default_value: None, default_value: None,
@@ -299,6 +300,7 @@ impl App {
status: crate::format::ItemStatus::Modified, status: crate::format::ItemStatus::Modified,
value_type: crate::format::ValueType::String, value_type: crate::format::ValueType::String,
}; };
self.vars.insert(insert_pos, new_item); self.vars.insert(insert_pos, new_item);
self.selected = insert_pos; self.selected = insert_pos;
self.sync_input_with_selected(); self.sync_input_with_selected();
@@ -313,7 +315,7 @@ impl App {
pub fn selected_is_array(&self) -> bool { pub fn selected_is_array(&self) -> bool {
self.vars.get(self.selected) self.vars.get(self.selected)
.map(|v| !v.is_group && v.path.contains('[')) .map(|v| !v.is_group && matches!(v.path.last(), Some(PathSegment::Index(_))))
.unwrap_or(false) .unwrap_or(false)
} }
@@ -345,29 +347,3 @@ impl App {
} }
} }
} }
fn parse_index(path: &str) -> Option<(&str, usize)> {
if let Some(end) = path.rfind(']') {
let segment = &path[..=end];
if let Some(start) = segment.rfind('[')
&& let Ok(idx) = segment[start + 1..end].parse::<usize>() {
// Return the base and index
return Some((&path[..start], idx));
}
}
None
}
/// Helper to find an array segment in a path given a base prefix.
fn find_array_segment<'a>(path: &'a str, base: &str) -> Option<(&'a str, usize, &'a str)> {
if !path.starts_with(base) {
return None;
}
let remaining = &path[base.len()..];
if remaining.starts_with('[')
&& let Some(end) = remaining.find(']')
&& let Ok(idx) = remaining[1..end].parse::<usize>() {
return Some((&path[..base.len()], idx, &remaining[end + 1..]));
}
None
}

View File

@@ -1,4 +1,4 @@
use super::{ConfigItem, FormatHandler, ItemStatus, ValueType}; use super::{ConfigItem, FormatHandler, ItemStatus, ValueType, PathSegment};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
@@ -18,9 +18,10 @@ impl FormatHandler for EnvHandler {
if let Some((key, val)) = line.split_once('=') { if let Some((key, val)) = line.split_once('=') {
let parsed_val = val.trim().trim_matches('"').trim_matches('\'').to_string(); let parsed_val = val.trim().trim_matches('"').trim_matches('\'').to_string();
let key_str = key.trim().to_string();
vars.push(ConfigItem { vars.push(ConfigItem {
key: key.trim().to_string(), key: key_str.clone(),
path: key.trim().to_string(), path: vec![PathSegment::Key(key_str)],
value: Some(parsed_val.clone()), value: Some(parsed_val.clone()),
template_value: Some(parsed_val.clone()), template_value: Some(parsed_val.clone()),
default_value: Some(parsed_val), default_value: Some(parsed_val),
@@ -116,7 +117,7 @@ mod tests {
let file = NamedTempFile::new().unwrap(); let file = NamedTempFile::new().unwrap();
let vars = vec![ConfigItem { let vars = vec![ConfigItem {
key: "KEY1".to_string(), key: "KEY1".to_string(),
path: "KEY1".to_string(), path: vec![PathSegment::Key("KEY1".to_string())],
value: Some("value1".to_string()), value: Some("value1".to_string()),
template_value: None, template_value: None,
default_value: None, default_value: None,

View File

@@ -1,4 +1,4 @@
use super::{ConfigItem, FormatHandler, FormatType, ItemStatus, ValueType}; use super::{ConfigItem, FormatHandler, FormatType, ItemStatus, ValueType, PathSegment};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
@@ -153,26 +153,33 @@ fn json_to_xml(value: &Value) -> String {
} }
} }
// remove unused get_xml_root_name fn flatten(value: &Value, current_path: Vec<PathSegment>, key_name: Option<String>, depth: usize, vars: &mut Vec<ConfigItem>) {
// fn get_xml_root_name(content: &str) -> Option<String> { ... } let mut next_path = current_path.clone();
fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut Vec<ConfigItem>) { if let Some(ref k) = key_name {
let path = if prefix.is_empty() { if !current_path.is_empty() {
key_name.to_string() // It's a key in an object, so append to path
} else if key_name.is_empty() { next_path.push(PathSegment::Key(k.clone()));
prefix.to_string() } else {
} else if key_name.starts_with('[') { // First element, maybe root
format!("{}{}", prefix, key_name) if !k.is_empty() {
} else { next_path.push(PathSegment::Key(k.clone()));
format!("{}.{}", prefix, key_name) }
}
}
let display_key = match next_path.last() {
Some(PathSegment::Key(k)) => k.clone(),
Some(PathSegment::Index(i)) => format!("[{}]", i),
None => "".to_string(),
}; };
match value { match value {
Value::Object(map) => { Value::Object(map) => {
if !path.is_empty() { if !next_path.is_empty() {
vars.push(ConfigItem { vars.push(ConfigItem {
key: key_name.to_string(), key: display_key,
path: path.clone(), path: next_path.clone(),
value: None, value: None,
template_value: None, template_value: None,
default_value: None, default_value: None,
@@ -182,16 +189,16 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut
value_type: ValueType::Null, value_type: ValueType::Null,
}); });
} }
let next_depth = if path.is_empty() { depth } else { depth + 1 }; let next_depth = if next_path.is_empty() { depth } else { depth + 1 };
for (k, v) in map { for (k, v) in map {
flatten(v, &path, next_depth, k, vars); flatten(v, next_path.clone(), Some(k.clone()), next_depth, vars);
} }
} }
Value::Array(arr) => { Value::Array(arr) => {
if !path.is_empty() { if !next_path.is_empty() {
vars.push(ConfigItem { vars.push(ConfigItem {
key: key_name.to_string(), key: display_key,
path: path.clone(), path: next_path.clone(),
value: None, value: None,
template_value: None, template_value: None,
default_value: None, default_value: None,
@@ -201,16 +208,17 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut
value_type: ValueType::Null, value_type: ValueType::Null,
}); });
} }
let next_depth = if path.is_empty() { depth } else { depth + 1 }; let next_depth = if next_path.is_empty() { depth } else { depth + 1 };
for (i, v) in arr.iter().enumerate() { for (i, v) in arr.iter().enumerate() {
let array_key = format!("[{}]", i); let mut arr_path = next_path.clone();
flatten(v, &path, next_depth, &array_key, vars); arr_path.push(PathSegment::Index(i));
flatten(v, arr_path, None, next_depth, vars);
} }
} }
Value::String(s) => { Value::String(s) => {
vars.push(ConfigItem { vars.push(ConfigItem {
key: key_name.to_string(), key: display_key,
path: path.clone(), path: next_path.clone(),
value: Some(s.clone()), value: Some(s.clone()),
template_value: Some(s.clone()), template_value: Some(s.clone()),
default_value: Some(s.clone()), default_value: Some(s.clone()),
@@ -223,8 +231,8 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut
Value::Number(n) => { Value::Number(n) => {
let s = n.to_string(); let s = n.to_string();
vars.push(ConfigItem { vars.push(ConfigItem {
key: key_name.to_string(), key: display_key,
path: path.clone(), path: next_path.clone(),
value: Some(s.clone()), value: Some(s.clone()),
template_value: Some(s.clone()), template_value: Some(s.clone()),
default_value: Some(s.clone()), default_value: Some(s.clone()),
@@ -237,8 +245,8 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut
Value::Bool(b) => { Value::Bool(b) => {
let s = b.to_string(); let s = b.to_string();
vars.push(ConfigItem { vars.push(ConfigItem {
key: key_name.to_string(), key: display_key,
path: path.clone(), path: next_path.clone(),
value: Some(s.clone()), value: Some(s.clone()),
template_value: Some(s.clone()), template_value: Some(s.clone()),
default_value: Some(s.clone()), default_value: Some(s.clone()),
@@ -250,8 +258,8 @@ fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut
} }
Value::Null => { Value::Null => {
vars.push(ConfigItem { vars.push(ConfigItem {
key: key_name.to_string(), key: display_key,
path: path.clone(), path: next_path.clone(),
value: Some("".to_string()), value: Some("".to_string()),
template_value: Some("".to_string()), template_value: Some("".to_string()),
default_value: Some("".to_string()), default_value: Some("".to_string()),
@@ -268,7 +276,7 @@ impl FormatHandler for HierarchicalHandler {
fn parse(&self, path: &Path) -> anyhow::Result<Vec<ConfigItem>> { fn parse(&self, path: &Path) -> anyhow::Result<Vec<ConfigItem>> {
let value = self.read_value(path)?; let value = self.read_value(path)?;
let mut vars = Vec::new(); let mut vars = Vec::new();
flatten(&value, "", 0, "", &mut vars); flatten(&value, Vec::new(), Some("".to_string()), 0, &mut vars);
Ok(vars) Ok(vars)
} }
@@ -286,50 +294,52 @@ impl FormatHandler for HierarchicalHandler {
} }
} }
fn insert_into_value(root: &mut Value, path: &str, new_val_str: &str, value_type: ValueType) { fn insert_into_value(root: &mut Value, path: &[PathSegment], new_val_str: &str, value_type: ValueType) {
let mut parts = path.split('.'); if path.is_empty() {
let last_part = match parts.next_back() { return;
Some(p) => p, }
None => return,
};
let mut current = root; let mut current = root;
for part in parts {
let (key, idx) = parse_array_key(part);
if !current.is_object() {
*current = Value::Object(Map::new());
}
let map = current.as_object_mut().unwrap();
let next_node = map.entry(key.to_string()).or_insert_with(|| { // Traverse all but the last segment
if idx.is_some() { for i in 0..path.len() - 1 {
Value::Array(Vec::new()) let segment = &path[i];
} else { let next_segment = &path[i + 1];
Value::Object(Map::new())
}
});
if let Some(i) = idx { match segment {
if !next_node.is_array() { PathSegment::Key(key) => {
*next_node = Value::Array(Vec::new()); if !current.is_object() {
*current = Value::Object(Map::new());
}
let map = current.as_object_mut().unwrap();
let next_node = map.entry(key.clone()).or_insert_with(|| {
match next_segment {
PathSegment::Index(_) => Value::Array(Vec::new()),
PathSegment::Key(_) => Value::Object(Map::new()),
}
});
current = next_node;
} }
let arr = next_node.as_array_mut().unwrap(); PathSegment::Index(idx) => {
while arr.len() <= i { if !current.is_array() {
arr.push(Value::Object(Map::new())); *current = Value::Array(Vec::new());
}
let arr = current.as_array_mut().unwrap();
while arr.len() <= *idx {
match next_segment {
PathSegment::Index(_) => arr.push(Value::Array(Vec::new())),
PathSegment::Key(_) => arr.push(Value::Object(Map::new())),
}
}
current = &mut arr[*idx];
} }
current = &mut arr[i];
} else {
current = next_node;
} }
} }
let (final_key, final_idx) = parse_array_key(last_part); // Handle the final segment
if !current.is_object() { let final_segment = &path[path.len() - 1];
*current = Value::Object(Map::new());
}
let map = current.as_object_mut().unwrap();
// Use the preserved ValueType instead of aggressive inference
let final_val = match value_type { let final_val = match value_type {
ValueType::Number => { ValueType::Number => {
if let Ok(n) = new_val_str.parse::<i64>() { if let Ok(n) = new_val_str.parse::<i64>() {
@@ -355,31 +365,24 @@ fn insert_into_value(root: &mut Value, path: &str, new_val_str: &str, value_type
_ => Value::String(new_val_str.to_string()), _ => Value::String(new_val_str.to_string()),
}; };
if let Some(i) = final_idx { match final_segment {
let next_node = map PathSegment::Key(key) => {
.entry(final_key.to_string()) if !current.is_object() {
.or_insert_with(|| Value::Array(Vec::new())); *current = Value::Object(Map::new());
if !next_node.is_array() { }
*next_node = Value::Array(Vec::new()); let map = current.as_object_mut().unwrap();
map.insert(key.clone(), final_val);
} }
let arr = next_node.as_array_mut().unwrap(); PathSegment::Index(idx) => {
while arr.len() <= i { if !current.is_array() {
arr.push(Value::Null); *current = Value::Array(Vec::new());
}
let arr = current.as_array_mut().unwrap();
while arr.len() <= *idx {
arr.push(Value::Null);
}
arr[*idx] = final_val;
} }
arr[i] = final_val;
} else {
map.insert(final_key.to_string(), final_val);
}
}
fn parse_array_key(part: &str) -> (&str, Option<usize>) {
if part.ends_with(']') && part.contains('[') {
let start_idx = part.find('[').unwrap();
let key = &part[..start_idx];
let idx = part[start_idx + 1..part.len() - 1].parse::<usize>().ok();
(key, idx)
} else {
(part, None)
} }
} }
@@ -401,7 +404,7 @@ mod tests {
} }
}); });
flatten(&json, "", 0, "", &mut vars); flatten(&json, Vec::new(), Some("".to_string()), 0, &mut vars);
assert_eq!(vars.len(), 6); assert_eq!(vars.len(), 6);
let mut root = Value::Object(Map::new()); let mut root = Value::Object(Map::new());
@@ -411,7 +414,6 @@ mod tests {
} }
} }
// When unflattening, it parses bool back
let unflattened_json = serde_json::to_string(&root).unwrap(); let unflattened_json = serde_json::to_string(&root).unwrap();
assert!(unflattened_json.contains("\"8080:80\"")); assert!(unflattened_json.contains("\"8080:80\""));
assert!(unflattened_json.contains("true")); assert!(unflattened_json.contains("true"));
@@ -420,7 +422,6 @@ mod tests {
#[test] #[test]
fn test_type_preservation() { fn test_type_preservation() {
let mut vars = Vec::new(); let mut vars = Vec::new();
// A JSON with various tricky types
let json = serde_json::json!({ let json = serde_json::json!({
"port_num": 8080, "port_num": 8080,
"port_str": "8080", "port_str": "8080",
@@ -430,7 +431,7 @@ mod tests {
"float_str": "42.42" "float_str": "42.42"
}); });
flatten(&json, "", 0, "", &mut vars); flatten(&json, Vec::new(), Some("".to_string()), 0, &mut vars);
let mut root = Value::Object(Map::new()); let mut root = Value::Object(Map::new());
for var in vars { for var in vars {
@@ -439,7 +440,6 @@ mod tests {
} }
} }
// Validate that types are exactly preserved after re-assembling
let unflattened = root.as_object().unwrap(); let unflattened = root.as_object().unwrap();
assert!(unflattened["port_num"].is_number(), "port_num should be a number"); assert!(unflattened["port_num"].is_number(), "port_num should be a number");
@@ -471,7 +471,7 @@ server:
"; ";
let yaml_val: Value = serde_yaml::from_str(yaml_str).unwrap(); let yaml_val: Value = serde_yaml::from_str(yaml_str).unwrap();
let mut vars = Vec::new(); let mut vars = Vec::new();
flatten(&yaml_val, "", 0, "", &mut vars); flatten(&yaml_val, Vec::new(), Some("".to_string()), 0, &mut vars);
let mut root = Value::Object(Map::new()); let mut root = Value::Object(Map::new());
for var in vars { for var in vars {
@@ -482,7 +482,6 @@ server:
let unflattened_yaml = serde_yaml::to_string(&root).unwrap(); let unflattened_yaml = serde_yaml::to_string(&root).unwrap();
assert!(unflattened_yaml.contains("port: 8080")); assert!(unflattened_yaml.contains("port: 8080"));
// Serde YAML might output '8080' or "8080"
assert!(unflattened_yaml.contains("port_str: '8080'") || unflattened_yaml.contains("port_str: \"8080\"")); assert!(unflattened_yaml.contains("port_str: '8080'") || unflattened_yaml.contains("port_str: \"8080\""));
assert!(unflattened_yaml.contains("enabled: true")); assert!(unflattened_yaml.contains("enabled: true"));
} }
@@ -495,12 +494,11 @@ port = 8080
port_str = \"8080\" port_str = \"8080\"
enabled = true enabled = true
"; ";
// parse to toml Value, then convert to serde_json Value to reuse the same flatten path
let toml_val: toml::Value = toml::from_str(toml_str).unwrap(); let toml_val: toml::Value = toml::from_str(toml_str).unwrap();
let json_val: Value = serde_json::to_value(toml_val).unwrap(); let json_val: Value = serde_json::to_value(toml_val).unwrap();
let mut vars = Vec::new(); let mut vars = Vec::new();
flatten(&json_val, "", 0, "", &mut vars); flatten(&json_val, Vec::new(), Some("".to_string()), 0, &mut vars);
let mut root = Value::Object(Map::new()); let mut root = Value::Object(Map::new());
for var in vars { for var in vars {
@@ -509,7 +507,6 @@ enabled = true
} }
} }
// Convert back to TOML
let toml_root: toml::Value = serde_json::from_value(root).unwrap(); let toml_root: toml::Value = serde_json::from_value(root).unwrap();
let unflattened_toml = toml::to_string(&toml_root).unwrap(); let unflattened_toml = toml::to_string(&toml_root).unwrap();
@@ -525,7 +522,7 @@ enabled = true
let json_val = xml_to_json(xml_str).unwrap(); let json_val = xml_to_json(xml_str).unwrap();
let mut vars = Vec::new(); let mut vars = Vec::new();
flatten(&json_val, "", 0, "", &mut vars); flatten(&json_val, Vec::new(), Some("".to_string()), 0, &mut vars);
let mut root = Value::Object(Map::new()); let mut root = Value::Object(Map::new());
for var in vars { for var in vars {
@@ -534,7 +531,6 @@ enabled = true
} }
} }
println!("Reconstructed root: {:?}", root);
let unflattened_xml = json_to_xml(&root); let unflattened_xml = json_to_xml(&root);
assert!(unflattened_xml.contains("<port>8080</port>")); assert!(unflattened_xml.contains("<port>8080</port>"));

View File

@@ -1,4 +1,4 @@
use super::{ConfigItem, FormatHandler, ItemStatus, ValueType}; use super::{ConfigItem, FormatHandler, ItemStatus, ValueType, PathSegment};
use ini::Ini; use ini::Ini;
use std::path::Path; use std::path::Path;
@@ -15,7 +15,7 @@ impl FormatHandler for IniHandler {
if !section_name.is_empty() { if !section_name.is_empty() {
vars.push(ConfigItem { vars.push(ConfigItem {
key: section_name.to_string(), key: section_name.to_string(),
path: section_name.to_string(), path: vec![PathSegment::Key(section_name.to_string())],
value: None, value: None,
template_value: None, template_value: None,
default_value: None, default_value: None,
@@ -28,9 +28,9 @@ impl FormatHandler for IniHandler {
for (key, value) in prop { for (key, value) in prop {
let path = if section_name.is_empty() { let path = if section_name.is_empty() {
key.to_string() vec![PathSegment::Key(key.to_string())]
} else { } else {
format!("{}.{}", section_name, key) vec![PathSegment::Key(section_name.to_string()), PathSegment::Key(key.to_string())]
}; };
vars.push(ConfigItem { vars.push(ConfigItem {
@@ -58,11 +58,14 @@ impl FormatHandler for IniHandler {
.or(var.template_value.as_deref()) .or(var.template_value.as_deref())
.unwrap_or(""); .unwrap_or("");
if let Some((section, key)) = var.path.split_once('.') { if var.path.len() == 2 {
conf.with_section(Some(section)).set(key, val); if let (PathSegment::Key(section), PathSegment::Key(key)) = (&var.path[0], &var.path[1]) {
} else { conf.with_section(Some(section)).set(key, val);
conf.with_section(None::<String>).set(&var.path, val); }
} } else if var.path.len() == 1
&& let PathSegment::Key(key) = &var.path[0] {
conf.with_section(None::<String>).set(key, val);
}
} }
} }
conf.write_to_file(path)?; conf.write_to_file(path)?;
@@ -84,8 +87,8 @@ mod tests {
let handler = IniHandler; let handler = IniHandler;
let vars = handler.parse(file.path()).unwrap(); let vars = handler.parse(file.path()).unwrap();
assert!(vars.iter().any(|v| v.path == "server" && v.is_group)); assert!(vars.iter().any(|v| v.path_string() == "server" && v.is_group));
assert!(vars.iter().any(|v| v.path == "server.port" && v.value.as_deref() == Some("8080"))); assert!(vars.iter().any(|v| v.path_string() == "server.port" && v.value.as_deref() == Some("8080")));
assert!(vars.iter().any(|v| v.path == "database.host" && v.value.as_deref() == Some("localhost"))); assert!(vars.iter().any(|v| v.path_string() == "database.host" && v.value.as_deref() == Some("localhost")));
} }
} }

View File

@@ -20,10 +20,25 @@ pub enum ValueType {
Null, Null,
} }
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PathSegment {
Key(String),
Index(usize),
}
impl std::fmt::Display for PathSegment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PathSegment::Key(k) => write!(f, "{}", k),
PathSegment::Index(i) => write!(f, "[{}]", i),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ConfigItem { pub struct ConfigItem {
pub key: String, pub key: String,
pub path: String, pub path: Vec<PathSegment>,
pub value: Option<String>, pub value: Option<String>,
pub template_value: Option<String>, pub template_value: Option<String>,
pub default_value: Option<String>, pub default_value: Option<String>,
@@ -33,6 +48,25 @@ pub struct ConfigItem {
pub value_type: ValueType, pub value_type: ValueType,
} }
impl ConfigItem {
pub fn path_string(&self) -> String {
let mut s = String::new();
for (i, segment) in self.path.iter().enumerate() {
match segment {
PathSegment::Key(k) => {
if i > 0 {
s.push('.');
}
s.push_str(k);
}
PathSegment::Index(idx) => {
s.push_str(&format!("[{}]", idx));
}
}
}
s
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormatType { pub enum FormatType {
Env, Env,

View File

@@ -1,4 +1,4 @@
use super::{ConfigItem, FormatHandler, ItemStatus, ValueType}; use super::{ConfigItem, FormatHandler, ItemStatus, ValueType, PathSegment};
use java_properties::{LineContent, PropertiesIter, PropertiesWriter}; use java_properties::{LineContent, PropertiesIter, PropertiesWriter};
use std::fs::File; use std::fs::File;
use std::io::{BufReader, BufWriter}; use std::io::{BufReader, BufWriter};
@@ -21,13 +21,10 @@ impl FormatHandler for PropertiesHandler {
if let LineContent::KVPair(path, value) = line.consume_content() { if let LineContent::KVPair(path, value) = line.consume_content() {
// Add groups based on dot notation // Add groups based on dot notation
let parts: Vec<&str> = path.split('.').collect(); let parts: Vec<&str> = path.split('.').collect();
let mut current_path = String::new(); let mut current_path = Vec::new();
for (i, part) in parts.iter().enumerate().take(parts.len().saturating_sub(1)) { for (i, part) in parts.iter().enumerate().take(parts.len().saturating_sub(1)) {
if !current_path.is_empty() { current_path.push(PathSegment::Key(part.to_string()));
current_path.push('.');
}
current_path.push_str(part);
if groups.insert(current_path.clone()) { if groups.insert(current_path.clone()) {
vars.push(ConfigItem { vars.push(ConfigItem {
@@ -44,9 +41,13 @@ impl FormatHandler for PropertiesHandler {
} }
} }
let mut final_path = current_path.clone();
let last_key = parts.last().unwrap_or(&"").to_string();
final_path.push(PathSegment::Key(last_key.clone()));
vars.push(ConfigItem { vars.push(ConfigItem {
key: parts.last().unwrap_or(&"").to_string(), key: last_key,
path: path.clone(), path: final_path,
value: Some(value.clone()), value: Some(value.clone()),
template_value: Some(value.clone()), template_value: Some(value.clone()),
default_value: Some(value.clone()), default_value: Some(value.clone()),
@@ -72,7 +73,7 @@ impl FormatHandler for PropertiesHandler {
let val = var.value.as_deref() let val = var.value.as_deref()
.or(var.template_value.as_deref()) .or(var.template_value.as_deref())
.unwrap_or(""); .unwrap_or("");
prop_writer.write(&var.path, val)?; prop_writer.write(&var.path_string(), val)?;
} }
} }
@@ -95,7 +96,7 @@ mod tests {
let handler = PropertiesHandler; let handler = PropertiesHandler;
let vars = handler.parse(file.path()).unwrap(); let vars = handler.parse(file.path()).unwrap();
assert!(vars.iter().any(|v| v.path == "server" && v.is_group)); assert!(vars.iter().any(|v| v.path_string() == "server" && v.is_group));
assert!(vars.iter().any(|v| v.path == "server.port" && v.value.as_deref() == Some("8080"))); assert!(vars.iter().any(|v| v.path_string() == "server.port" && v.value.as_deref() == Some("8080")));
} }
} }

View File

@@ -189,9 +189,9 @@ pub fn draw(f: &mut Frame, app: &mut App, config: &Config) {
if let Some(var) = current_var { if let Some(var) = current_var {
if var.is_group { if var.is_group {
input_title = format!(" Group: {} ", var.path); input_title = format!(" Group: {} ", var.path_string());
} else { } else {
input_title = format!(" Editing: {} ", var.path); input_title = format!(" Editing: {} ", var.path_string());
if let Some(t_val) = &var.template_value { if let Some(t_val) = &var.template_value {
extra_info = format!(" [Template: {}]", t_val); extra_info = format!(" [Template: {}]", t_val);
} }