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 super::{ConfigItem, FormatHandler, ItemStatus, ValueType};
use super::{ConfigItem, FormatHandler, ItemStatus, ValueType, PathSegment};
use std::fs;
use std::io::Write;
use std::path::Path;
@@ -18,9 +18,10 @@ impl FormatHandler for EnvHandler {
if let Some((key, val)) = line.split_once('=') {
let parsed_val = val.trim().trim_matches('"').trim_matches('\'').to_string();
let key_str = key.trim().to_string();
vars.push(ConfigItem {
key: key.trim().to_string(),
path: key.trim().to_string(),
key: key_str.clone(),
path: vec![PathSegment::Key(key_str)],
value: Some(parsed_val.clone()),
template_value: Some(parsed_val.clone()),
default_value: Some(parsed_val),
@@ -116,7 +117,7 @@ mod tests {
let file = NamedTempFile::new().unwrap();
let vars = vec![ConfigItem {
key: "KEY1".to_string(),
path: "KEY1".to_string(),
path: vec![PathSegment::Key("KEY1".to_string())],
value: Some("value1".to_string()),
template_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 std::fs;
use std::path::Path;
@@ -153,26 +153,33 @@ fn json_to_xml(value: &Value) -> String {
}
}
// remove unused get_xml_root_name
// fn get_xml_root_name(content: &str) -> Option<String> { ... }
fn flatten(value: &Value, current_path: Vec<PathSegment>, key_name: Option<String>, depth: usize, vars: &mut Vec<ConfigItem>) {
let mut next_path = current_path.clone();
if let Some(ref k) = key_name {
if !current_path.is_empty() {
// It's a key in an object, so append to path
next_path.push(PathSegment::Key(k.clone()));
} else {
// First element, maybe root
if !k.is_empty() {
next_path.push(PathSegment::Key(k.clone()));
}
}
}
fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut Vec<ConfigItem>) {
let path = if prefix.is_empty() {
key_name.to_string()
} else if key_name.is_empty() {
prefix.to_string()
} else if key_name.starts_with('[') {
format!("{}{}", prefix, key_name)
} else {
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 {
Value::Object(map) => {
if !path.is_empty() {
if !next_path.is_empty() {
vars.push(ConfigItem {
key: key_name.to_string(),
path: path.clone(),
key: display_key,
path: next_path.clone(),
value: None,
template_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,
});
}
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 {
flatten(v, &path, next_depth, k, vars);
flatten(v, next_path.clone(), Some(k.clone()), next_depth, vars);
}
}
Value::Array(arr) => {
if !path.is_empty() {
if !next_path.is_empty() {
vars.push(ConfigItem {
key: key_name.to_string(),
path: path.clone(),
key: display_key,
path: next_path.clone(),
value: None,
template_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,
});
}
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() {
let array_key = format!("[{}]", i);
flatten(v, &path, next_depth, &array_key, vars);
let mut arr_path = next_path.clone();
arr_path.push(PathSegment::Index(i));
flatten(v, arr_path, None, next_depth, vars);
}
}
Value::String(s) => {
vars.push(ConfigItem {
key: key_name.to_string(),
path: path.clone(),
key: display_key,
path: next_path.clone(),
value: Some(s.clone()),
template_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) => {
let s = n.to_string();
vars.push(ConfigItem {
key: key_name.to_string(),
path: path.clone(),
key: display_key,
path: next_path.clone(),
value: Some(s.clone()),
template_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) => {
let s = b.to_string();
vars.push(ConfigItem {
key: key_name.to_string(),
path: path.clone(),
key: display_key,
path: next_path.clone(),
value: Some(s.clone()),
template_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 => {
vars.push(ConfigItem {
key: key_name.to_string(),
path: path.clone(),
key: display_key,
path: next_path.clone(),
value: Some("".to_string()),
template_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>> {
let value = self.read_value(path)?;
let mut vars = Vec::new();
flatten(&value, "", 0, "", &mut vars);
flatten(&value, Vec::new(), Some("".to_string()), 0, &mut 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) {
let mut parts = path.split('.');
let last_part = match parts.next_back() {
Some(p) => p,
None => return,
};
fn insert_into_value(root: &mut Value, path: &[PathSegment], new_val_str: &str, value_type: ValueType) {
if path.is_empty() {
return;
}
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();
// Traverse all but the last segment
for i in 0..path.len() - 1 {
let segment = &path[i];
let next_segment = &path[i + 1];
let next_node = map.entry(key.to_string()).or_insert_with(|| {
if idx.is_some() {
Value::Array(Vec::new())
} else {
Value::Object(Map::new())
match segment {
PathSegment::Key(key) => {
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;
}
});
if let Some(i) = idx {
if !next_node.is_array() {
*next_node = Value::Array(Vec::new());
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 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;
}
}
let (final_key, final_idx) = parse_array_key(last_part);
if !current.is_object() {
*current = Value::Object(Map::new());
}
let map = current.as_object_mut().unwrap();
// Use the preserved ValueType instead of aggressive inference
// Handle the final segment
let final_segment = &path[path.len() - 1];
let final_val = match value_type {
ValueType::Number => {
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()),
};
if let Some(i) = final_idx {
let next_node = map
.entry(final_key.to_string())
.or_insert_with(|| Value::Array(Vec::new()));
if !next_node.is_array() {
*next_node = Value::Array(Vec::new());
match final_segment {
PathSegment::Key(key) => {
if !current.is_object() {
*current = Value::Object(Map::new());
}
let map = current.as_object_mut().unwrap();
map.insert(key.clone(), final_val);
}
let arr = next_node.as_array_mut().unwrap();
while arr.len() <= i {
arr.push(Value::Null);
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[*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);
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();
assert!(unflattened_json.contains("\"8080:80\""));
assert!(unflattened_json.contains("true"));
@@ -420,7 +422,6 @@ mod tests {
#[test]
fn test_type_preservation() {
let mut vars = Vec::new();
// A JSON with various tricky types
let json = serde_json::json!({
"port_num": 8080,
"port_str": "8080",
@@ -430,7 +431,7 @@ mod tests {
"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());
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();
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 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());
for var in vars {
@@ -482,7 +482,6 @@ server:
let unflattened_yaml = serde_yaml::to_string(&root).unwrap();
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("enabled: true"));
}
@@ -495,12 +494,11 @@ port = 8080
port_str = \"8080\"
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 json_val: Value = serde_json::to_value(toml_val).unwrap();
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());
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 unflattened_toml = toml::to_string(&toml_root).unwrap();
@@ -525,7 +522,7 @@ enabled = true
let json_val = xml_to_json(xml_str).unwrap();
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());
for var in vars {
@@ -534,7 +531,6 @@ enabled = true
}
}
println!("Reconstructed root: {:?}", root);
let unflattened_xml = json_to_xml(&root);
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 std::path::Path;
@@ -15,7 +15,7 @@ impl FormatHandler for IniHandler {
if !section_name.is_empty() {
vars.push(ConfigItem {
key: section_name.to_string(),
path: section_name.to_string(),
path: vec![PathSegment::Key(section_name.to_string())],
value: None,
template_value: None,
default_value: None,
@@ -28,9 +28,9 @@ impl FormatHandler for IniHandler {
for (key, value) in prop {
let path = if section_name.is_empty() {
key.to_string()
vec![PathSegment::Key(key.to_string())]
} else {
format!("{}.{}", section_name, key)
vec![PathSegment::Key(section_name.to_string()), PathSegment::Key(key.to_string())]
};
vars.push(ConfigItem {
@@ -58,11 +58,14 @@ impl FormatHandler for IniHandler {
.or(var.template_value.as_deref())
.unwrap_or("");
if let Some((section, key)) = var.path.split_once('.') {
conf.with_section(Some(section)).set(key, val);
} else {
conf.with_section(None::<String>).set(&var.path, val);
}
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);
}
} 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)?;
@@ -84,8 +87,8 @@ mod tests {
let handler = IniHandler;
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 == "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() == "server" && v.is_group));
assert!(vars.iter().any(|v| v.path_string() == "server.port" && v.value.as_deref() == Some("8080")));
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,
}
#[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)]
pub struct ConfigItem {
pub key: String,
pub path: String,
pub path: Vec<PathSegment>,
pub value: Option<String>,
pub template_value: Option<String>,
pub default_value: Option<String>,
@@ -33,6 +48,25 @@ pub struct ConfigItem {
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)]
pub enum FormatType {
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 std::fs::File;
use std::io::{BufReader, BufWriter};
@@ -21,13 +21,10 @@ impl FormatHandler for PropertiesHandler {
if let LineContent::KVPair(path, value) = line.consume_content() {
// Add groups based on dot notation
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)) {
if !current_path.is_empty() {
current_path.push('.');
}
current_path.push_str(part);
current_path.push(PathSegment::Key(part.to_string()));
if groups.insert(current_path.clone()) {
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 {
key: parts.last().unwrap_or(&"").to_string(),
path: path.clone(),
key: last_key,
path: final_path,
value: Some(value.clone()),
template_value: Some(value.clone()),
default_value: Some(value.clone()),
@@ -72,7 +73,7 @@ impl FormatHandler for PropertiesHandler {
let val = var.value.as_deref()
.or(var.template_value.as_deref())
.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 vars = handler.parse(file.path()).unwrap();
assert!(vars.iter().any(|v| v.path == "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" && v.is_group));
assert!(vars.iter().any(|v| v.path_string() == "server.port" && v.value.as_deref() == Some("8080")));
}
}