release/0.5.0 #15
82
src/app.rs
82
src/app.rs
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 if key_name.starts_with('[') {
|
|
||||||
format!("{}{}", prefix, key_name)
|
|
||||||
} else {
|
} else {
|
||||||
format!("{}.{}", prefix, key_name)
|
// First element, maybe root
|
||||||
|
if !k.is_empty() {
|
||||||
|
next_path.push(PathSegment::Key(k.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
// Traverse all but the last segment
|
||||||
|
for i in 0..path.len() - 1 {
|
||||||
|
let segment = &path[i];
|
||||||
|
let next_segment = &path[i + 1];
|
||||||
|
|
||||||
|
match segment {
|
||||||
|
PathSegment::Key(key) => {
|
||||||
if !current.is_object() {
|
if !current.is_object() {
|
||||||
*current = Value::Object(Map::new());
|
*current = Value::Object(Map::new());
|
||||||
}
|
}
|
||||||
let map = current.as_object_mut().unwrap();
|
let map = current.as_object_mut().unwrap();
|
||||||
|
|
||||||
let next_node = map.entry(key.to_string()).or_insert_with(|| {
|
let next_node = map.entry(key.clone()).or_insert_with(|| {
|
||||||
if idx.is_some() {
|
match next_segment {
|
||||||
Value::Array(Vec::new())
|
PathSegment::Index(_) => Value::Array(Vec::new()),
|
||||||
} else {
|
PathSegment::Key(_) => Value::Object(Map::new()),
|
||||||
Value::Object(Map::new())
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(i) = idx {
|
|
||||||
if !next_node.is_array() {
|
|
||||||
*next_node = Value::Array(Vec::new());
|
|
||||||
}
|
|
||||||
let arr = next_node.as_array_mut().unwrap();
|
|
||||||
while arr.len() <= i {
|
|
||||||
arr.push(Value::Object(Map::new()));
|
|
||||||
}
|
|
||||||
current = &mut arr[i];
|
|
||||||
} else {
|
|
||||||
current = next_node;
|
current = next_node;
|
||||||
}
|
}
|
||||||
|
PathSegment::Index(idx) => {
|
||||||
|
if !current.is_array() {
|
||||||
|
*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];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,32 +365,25 @@ 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 arr = next_node.as_array_mut().unwrap();
|
let map = current.as_object_mut().unwrap();
|
||||||
while arr.len() <= i {
|
map.insert(key.clone(), final_val);
|
||||||
|
}
|
||||||
|
PathSegment::Index(idx) => {
|
||||||
|
if !current.is_array() {
|
||||||
|
*current = Value::Array(Vec::new());
|
||||||
|
}
|
||||||
|
let arr = current.as_array_mut().unwrap();
|
||||||
|
while arr.len() <= *idx {
|
||||||
arr.push(Value::Null);
|
arr.push(Value::Null);
|
||||||
}
|
}
|
||||||
arr[i] = final_val;
|
arr[*idx] = 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -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>"));
|
||||||
|
|||||||
@@ -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,10 +58,13 @@ 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 {
|
||||||
|
if let (PathSegment::Key(section), PathSegment::Key(key)) = (&var.path[0], &var.path[1]) {
|
||||||
conf.with_section(Some(section)).set(key, val);
|
conf.with_section(Some(section)).set(key, val);
|
||||||
} else {
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user