Compare commits
2 Commits
v0.4.3
...
a58906a2a4
| Author | SHA1 | Date | |
|---|---|---|---|
| a58906a2a4 | |||
| cffea6d806 |
125
Cargo.lock
generated
125
Cargo.lock
generated
@@ -247,6 +247,26 @@ dependencies = [
|
|||||||
"static_assertions",
|
"static_assertions",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||||
|
dependencies = [
|
||||||
|
"const-random-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random-macro"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"once_cell",
|
||||||
|
"tiny-keccak",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -292,6 +312,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -414,6 +440,15 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dlv-list"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||||
|
dependencies = [
|
||||||
|
"const-random",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@@ -429,6 +464,15 @@ version = "1.15.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -580,6 +624,12 @@ dependencies = [
|
|||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -679,6 +729,17 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "java-properties"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37bf6f484471c451f2b51eabd9e66b3fa7274550c5ec4b6c3d6070840945117f"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"lazy_static",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jiff"
|
name = "jiff"
|
||||||
version = "0.2.23"
|
version = "0.2.23"
|
||||||
@@ -860,8 +921,11 @@ dependencies = [
|
|||||||
"crossterm",
|
"crossterm",
|
||||||
"dirs",
|
"dirs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"java-properties",
|
||||||
"log",
|
"log",
|
||||||
|
"quick-xml",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"rust-ini",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
@@ -956,6 +1020,16 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-multimap"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||||
|
dependencies = [
|
||||||
|
"dlv-list",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -1114,6 +1188,16 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.39.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@@ -1284,6 +1368,16 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-ini"
|
||||||
|
version = "0.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"ordered-multimap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -1642,10 +1736,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "tiny-keccak"
|
||||||
version = "1.0.6+spec-1.1.0"
|
version = "2.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc"
|
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||||
|
dependencies = [
|
||||||
|
"crunchy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "1.0.7+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
@@ -1658,27 +1761,27 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.0.0+spec-1.1.0"
|
version = "1.0.1+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.0.9+spec-1.1.0"
|
version = "1.0.10+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.0.6+spec-1.1.0"
|
version = "1.0.7+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tui-input"
|
name = "tui-input"
|
||||||
@@ -1990,9 +2093,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.7.15"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
|
|||||||
17
Cargo.toml
17
Cargo.toml
@@ -1,3 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["Nils Pukropp <nils@narl.io>"]
|
||||||
|
edition = "2024"
|
||||||
|
name = "mould"
|
||||||
|
version = "0.5.0"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "mould"
|
name = "mould"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
@@ -7,12 +13,15 @@ anyhow = "1.0.102"
|
|||||||
crossterm = "0.29.0"
|
crossterm = "0.29.0"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
env_logger = "0.11.9"
|
env_logger = "0.11.9"
|
||||||
|
java-properties = "2.0.0"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
|
quick-xml = { version = "0.39.2", features = ["serde", "serialize"] }
|
||||||
ratatui = "0.30.0"
|
ratatui = "0.30.0"
|
||||||
|
rust-ini = "0.21.3"
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
serde_yaml = "0.9.34"
|
serde_yaml = "0.9.34"
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
toml = "1.0.6"
|
toml = "1.0.7"
|
||||||
tui-input = "0.15.0"
|
tui-input = "0.15.0"
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
@@ -25,9 +34,3 @@ version = "1.0.228"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.27.0"
|
tempfile = "3.27.0"
|
||||||
|
|
||||||
[package]
|
|
||||||
authors = ["Nils Pukropp <nils@narl.io>"]
|
|
||||||
edition = "2024"
|
|
||||||
name = "mould"
|
|
||||||
version = "0.4.3"
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
# mould
|
# mould
|
||||||
|
|
||||||
mould is a modern Terminal User Interface (TUI) tool designed for interactively generating and editing configuration files from templates. Whether you are setting up a `.env` file from an example, creating a `docker-compose.override.yml`, or editing nested `JSON`, `YAML`, or `TOML` configurations, mould provides a fast, Vim-inspired workflow to get your environment ready.
|
mould is a modern Terminal User Interface (TUI) tool designed for interactively generating and editing configuration files from templates. Whether you are setting up a `.env` file from an example, creating a `docker-compose.override.yml`, or editing nested `JSON`, `YAML`, `TOML`, `XML`, `INI`, or `Properties` configurations, mould provides a fast, Vim-inspired workflow to get your environment ready.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Universal Format Support**: Handle `.env`, `JSON`, `YAML`, and `TOML` seamlessly.
|
- **Universal Format Support**: Handle `.env`, `JSON`, `YAML`, `TOML`, `XML`, `INI`, and `Properties` seamlessly.
|
||||||
- **Tree View Navigation**: Edit nested data structures (JSON, YAML, TOML) in a beautiful, depth-colored tree view.
|
- **Tree View Navigation**: Edit nested data structures in a beautiful, depth-colored tree view.
|
||||||
- **Smart Template Comparison**: Automatically discovers `.env.example` vs `.env` relationships and highlights missing or modified keys.
|
- **Smart Template Discovery**: Rule-based resolver automatically discovers relationships (e.g., `.env.example` vs `.env`, `config.template.properties` vs `config.properties`) and highlights differences.
|
||||||
|
- **Strict Type Preservation**: Intelligently preserves data types (integers, booleans, strings) during edit-save cycles, ensuring your configuration stays valid.
|
||||||
- **Add Missing Keys**: Instantly pull missing keys and their default values from your template into your active configuration with a single keystroke.
|
- **Add Missing Keys**: Instantly pull missing keys and their default values from your template into your active configuration with a single keystroke.
|
||||||
- **Neovim Integration**: Comes with a built-in Neovim plugin for seamless in-editor configuration management.
|
- **Neovim Integration**: Comes with a built-in Neovim plugin for seamless in-editor configuration management.
|
||||||
- **Docker Compose Integration**: Automatically generate `docker-compose.override.yml` from `docker-compose.yml`.
|
- **Docker Compose Integration**: Automatically generate `docker-compose.override.yml` from `docker-compose.yml`.
|
||||||
|
|||||||
37
config.toml
Normal file
37
config.toml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[keybinds]
|
||||||
|
append_item = "o"
|
||||||
|
delete_item = "dd"
|
||||||
|
down = "j"
|
||||||
|
edit = "i"
|
||||||
|
edit_append = "A"
|
||||||
|
edit_substitute = "S"
|
||||||
|
jump_bottom = "G"
|
||||||
|
jump_top = "gg"
|
||||||
|
next_match = "n"
|
||||||
|
normal_mode = "Esc"
|
||||||
|
prepend_item = "O"
|
||||||
|
previous_match = "N"
|
||||||
|
quit = ":q"
|
||||||
|
save = ":w"
|
||||||
|
search = "/"
|
||||||
|
undo = "u"
|
||||||
|
up = "k"
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
bg_active = "#a6e3a1"
|
||||||
|
bg_highlight = "#89b4fa"
|
||||||
|
bg_normal = "#1e1e2e"
|
||||||
|
bg_search = "#cba6f7"
|
||||||
|
border_active = "#a6e3a1"
|
||||||
|
border_normal = "#45475a"
|
||||||
|
fg_accent = "#b4befe"
|
||||||
|
fg_dimmed = "#6c7086"
|
||||||
|
fg_highlight = "#1e1e2e"
|
||||||
|
fg_modified = "#fab387"
|
||||||
|
fg_normal = "#cdd6f4"
|
||||||
|
fg_warning = "#f38ba8"
|
||||||
|
transparent = true
|
||||||
|
tree_depth_1 = "#b4befe"
|
||||||
|
tree_depth_2 = "#cba6f7"
|
||||||
|
tree_depth_3 = "#89b4fa"
|
||||||
|
tree_depth_4 = "#fab387"
|
||||||
@@ -22,6 +22,8 @@ impl HierarchicalHandler {
|
|||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
|
||||||
FormatType::Toml => toml::from_str(&content)
|
FormatType::Toml => toml::from_str(&content)
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
|
||||||
|
FormatType::Xml => xml_to_json(&content)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
Ok(value)
|
Ok(value)
|
||||||
@@ -47,12 +49,124 @@ impl HierarchicalHandler {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
FormatType::Xml => json_to_xml(value),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
fs::write(path, content)
|
fs::write(path, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn xml_to_json(content: &str) -> io::Result<Value> {
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
|
||||||
|
let mut reader = Reader::from_str(content);
|
||||||
|
reader.config_mut().trim_text(true);
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
fn parse_recursive(reader: &mut Reader<&[u8]>) -> io::Result<Value> {
|
||||||
|
let mut map = Map::new();
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(e)) => {
|
||||||
|
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
|
||||||
|
let val = parse_recursive(reader)?;
|
||||||
|
|
||||||
|
if let Some(existing) = map.get_mut(&name) {
|
||||||
|
if let Some(arr) = existing.as_array_mut() {
|
||||||
|
arr.push(val);
|
||||||
|
} else {
|
||||||
|
let old = existing.take();
|
||||||
|
*existing = Value::Array(vec![old, val]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
map.insert(name, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(_)) => break,
|
||||||
|
Ok(Event::Text(e)) => {
|
||||||
|
text.push_str(&String::from_utf8_lossy(e.as_ref()));
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if map.is_empty() {
|
||||||
|
if text.is_empty() {
|
||||||
|
Ok(Value::Null)
|
||||||
|
} else {
|
||||||
|
Ok(Value::String(text))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !text.is_empty() {
|
||||||
|
map.insert("$text".to_string(), Value::String(text));
|
||||||
|
}
|
||||||
|
Ok(Value::Object(map))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the first start tag
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(e)) => {
|
||||||
|
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
|
||||||
|
let val = parse_recursive(&mut reader)?;
|
||||||
|
let mut root = Map::new();
|
||||||
|
root.insert(name, val);
|
||||||
|
return Ok(Value::Object(root));
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Value::Object(Map::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_to_xml(value: &Value) -> String {
|
||||||
|
match value {
|
||||||
|
Value::Object(map) => {
|
||||||
|
let mut s = String::new();
|
||||||
|
for (k, v) in map {
|
||||||
|
if k == "$text" {
|
||||||
|
s.push_str(&v.as_str().unwrap_or(""));
|
||||||
|
} else if let Some(arr) = v.as_array() {
|
||||||
|
for item in arr {
|
||||||
|
s.push_str(&format!("<{}>", k));
|
||||||
|
s.push_str(&json_to_xml(item));
|
||||||
|
s.push_str(&format!("</{}>", k));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.push_str(&format!("<{}>", k));
|
||||||
|
s.push_str(&json_to_xml(v));
|
||||||
|
s.push_str(&format!("</{}>", k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
Value::Array(arr) => {
|
||||||
|
let mut s = String::new();
|
||||||
|
for v in arr {
|
||||||
|
s.push_str(&json_to_xml(v));
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
Value::String(v) => v.clone(),
|
||||||
|
Value::Number(v) => v.to_string(),
|
||||||
|
Value::Bool(v) => v.to_string(),
|
||||||
|
Value::Null => "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove unused get_xml_root_name
|
||||||
|
// fn get_xml_root_name(content: &str) -> Option<String> { ... }
|
||||||
|
|
||||||
fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut Vec<ConfigItem>) {
|
fn flatten(value: &Value, prefix: &str, depth: usize, key_name: &str, vars: &mut Vec<ConfigItem>) {
|
||||||
let path = if prefix.is_empty() {
|
let path = if prefix.is_empty() {
|
||||||
key_name.to_string()
|
key_name.to_string()
|
||||||
@@ -322,7 +436,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_flatten_unflatten() {
|
fn test_json_flatten_unflatten() {
|
||||||
let mut vars = Vec::new();
|
let mut vars = Vec::new();
|
||||||
let json = serde_json::json!({
|
let json = serde_json::json!({
|
||||||
"services": {
|
"services": {
|
||||||
@@ -350,4 +464,129 @@ mod tests {
|
|||||||
assert!(unflattened_json.contains("\"8080:80\""));
|
assert!(unflattened_json.contains("\"8080:80\""));
|
||||||
assert!(unflattened_json.contains("true"));
|
assert!(unflattened_json.contains("true"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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",
|
||||||
|
"is_enabled": true,
|
||||||
|
"is_enabled_str": "true",
|
||||||
|
"float_num": 3.14,
|
||||||
|
"float_str": "3.14"
|
||||||
|
});
|
||||||
|
|
||||||
|
flatten(&json, "", 0, "", &mut vars);
|
||||||
|
|
||||||
|
let mut root = Value::Object(Map::new());
|
||||||
|
for var in vars {
|
||||||
|
if !var.is_group {
|
||||||
|
insert_into_value(&mut root, &var.path, var.value.as_deref().unwrap_or(""), var.value_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
assert_eq!(unflattened["port_num"].as_i64(), Some(8080));
|
||||||
|
|
||||||
|
assert!(unflattened["port_str"].is_string(), "port_str should be a string");
|
||||||
|
assert_eq!(unflattened["port_str"].as_str(), Some("8080"));
|
||||||
|
|
||||||
|
assert!(unflattened["is_enabled"].is_boolean(), "is_enabled should be a boolean");
|
||||||
|
assert_eq!(unflattened["is_enabled"].as_bool(), Some(true));
|
||||||
|
|
||||||
|
assert!(unflattened["is_enabled_str"].is_string(), "is_enabled_str should be a string");
|
||||||
|
assert_eq!(unflattened["is_enabled_str"].as_str(), Some("true"));
|
||||||
|
|
||||||
|
assert!(unflattened["float_num"].is_number(), "float_num should be a number");
|
||||||
|
assert_eq!(unflattened["float_num"].as_f64(), Some(3.14));
|
||||||
|
|
||||||
|
assert!(unflattened["float_str"].is_string(), "float_str should be a string");
|
||||||
|
assert_eq!(unflattened["float_str"].as_str(), Some("3.14"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_yaml_flatten_unflatten() {
|
||||||
|
let yaml_str = "
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
port_str: \"8080\"
|
||||||
|
enabled: true
|
||||||
|
";
|
||||||
|
let yaml_val: Value = serde_yaml::from_str(yaml_str).unwrap();
|
||||||
|
let mut vars = Vec::new();
|
||||||
|
flatten(&yaml_val, "", 0, "", &mut vars);
|
||||||
|
|
||||||
|
let mut root = Value::Object(Map::new());
|
||||||
|
for var in vars {
|
||||||
|
if !var.is_group {
|
||||||
|
insert_into_value(&mut root, &var.path, var.value.as_deref().unwrap_or(""), var.value_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_toml_flatten_unflatten() {
|
||||||
|
let toml_str = "
|
||||||
|
[server]
|
||||||
|
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);
|
||||||
|
|
||||||
|
let mut root = Value::Object(Map::new());
|
||||||
|
for var in vars {
|
||||||
|
if !var.is_group {
|
||||||
|
insert_into_value(&mut root, &var.path, var.value.as_deref().unwrap_or(""), var.value_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert back to TOML
|
||||||
|
let toml_root: toml::Value = serde_json::from_value(root).unwrap();
|
||||||
|
let unflattened_toml = toml::to_string(&toml_root).unwrap();
|
||||||
|
|
||||||
|
assert!(unflattened_toml.contains("port = 8080"));
|
||||||
|
assert!(unflattened_toml.contains("port_str = \"8080\""));
|
||||||
|
assert!(unflattened_toml.contains("enabled = true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xml_flatten_unflatten() {
|
||||||
|
let xml_str = "<config><server><port>8080</port><enabled>true</enabled></server></config>";
|
||||||
|
|
||||||
|
let json_val = xml_to_json(xml_str).unwrap();
|
||||||
|
|
||||||
|
let mut vars = Vec::new();
|
||||||
|
flatten(&json_val, "", 0, "", &mut vars);
|
||||||
|
|
||||||
|
let mut root = Value::Object(Map::new());
|
||||||
|
for var in vars {
|
||||||
|
if !var.is_group {
|
||||||
|
insert_into_value(&mut root, &var.path, var.value.as_deref().unwrap_or(""), var.value_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Reconstructed root: {:?}", root);
|
||||||
|
let unflattened_xml = json_to_xml(&root);
|
||||||
|
|
||||||
|
assert!(unflattened_xml.contains("<port>8080</port>"));
|
||||||
|
assert!(unflattened_xml.contains("<enabled>true</enabled>"));
|
||||||
|
assert!(unflattened_xml.contains("<config>") && unflattened_xml.contains("</config>"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
125
src/format/ini.rs
Normal file
125
src/format/ini.rs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
use super::{ConfigItem, FormatHandler, ItemStatus, ValueType};
|
||||||
|
use ini::Ini;
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub struct IniHandler;
|
||||||
|
|
||||||
|
impl FormatHandler for IniHandler {
|
||||||
|
fn parse(&self, path: &Path) -> io::Result<Vec<ConfigItem>> {
|
||||||
|
let conf = Ini::load_from_file(path)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||||
|
let mut vars = Vec::new();
|
||||||
|
|
||||||
|
for (section, prop) in &conf {
|
||||||
|
let section_name = section.unwrap_or_default();
|
||||||
|
|
||||||
|
if !section_name.is_empty() {
|
||||||
|
vars.push(ConfigItem {
|
||||||
|
key: section_name.to_string(),
|
||||||
|
path: section_name.to_string(),
|
||||||
|
value: None,
|
||||||
|
template_value: None,
|
||||||
|
default_value: None,
|
||||||
|
depth: 0,
|
||||||
|
is_group: true,
|
||||||
|
status: ItemStatus::Present,
|
||||||
|
value_type: ValueType::Null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in prop {
|
||||||
|
let path = if section_name.is_empty() {
|
||||||
|
key.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}.{}", section_name, key)
|
||||||
|
};
|
||||||
|
|
||||||
|
vars.push(ConfigItem {
|
||||||
|
key: key.to_string(),
|
||||||
|
path,
|
||||||
|
value: Some(value.to_string()),
|
||||||
|
template_value: Some(value.to_string()),
|
||||||
|
default_value: Some(value.to_string()),
|
||||||
|
depth: if section_name.is_empty() { 0 } else { 1 },
|
||||||
|
is_group: false,
|
||||||
|
status: ItemStatus::Present,
|
||||||
|
value_type: ValueType::String,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(vars)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge(&self, path: &Path, vars: &mut Vec<ConfigItem>) -> io::Result<()> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing_vars = self.parse(path).unwrap_or_default();
|
||||||
|
|
||||||
|
for var in vars.iter_mut() {
|
||||||
|
if let Some(existing) = existing_vars.iter().find(|v| v.path == var.path) {
|
||||||
|
if var.value != existing.value {
|
||||||
|
var.value = existing.value.clone();
|
||||||
|
var.status = ItemStatus::Modified;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var.status = ItemStatus::MissingFromActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add items from active that are not in template
|
||||||
|
for existing in existing_vars {
|
||||||
|
if !vars.iter().any(|v| v.path == existing.path) {
|
||||||
|
let mut new_item = existing.clone();
|
||||||
|
new_item.status = ItemStatus::MissingFromTemplate;
|
||||||
|
new_item.template_value = None;
|
||||||
|
new_item.default_value = None;
|
||||||
|
vars.push(new_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self, path: &Path, vars: &[ConfigItem]) -> io::Result<()> {
|
||||||
|
let mut conf = Ini::new();
|
||||||
|
for var in vars {
|
||||||
|
if !var.is_group {
|
||||||
|
let val = var.value.as_deref()
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conf.write_to_file(path)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_ini() {
|
||||||
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
writeln!(file, "[server]\nport=8080\n[database]\nhost=localhost").unwrap();
|
||||||
|
|
||||||
|
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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ use std::path::Path;
|
|||||||
|
|
||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod hierarchical;
|
pub mod hierarchical;
|
||||||
|
pub mod ini;
|
||||||
|
pub mod properties;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ItemStatus {
|
pub enum ItemStatus {
|
||||||
@@ -39,6 +41,9 @@ pub enum FormatType {
|
|||||||
Json,
|
Json,
|
||||||
Yaml,
|
Yaml,
|
||||||
Toml,
|
Toml,
|
||||||
|
Xml,
|
||||||
|
Ini,
|
||||||
|
Properties,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FormatHandler {
|
pub trait FormatHandler {
|
||||||
@@ -54,6 +59,9 @@ pub fn detect_format(path: &Path, override_format: Option<String>) -> FormatType
|
|||||||
"json" => return FormatType::Json,
|
"json" => return FormatType::Json,
|
||||||
"yaml" | "yml" => return FormatType::Yaml,
|
"yaml" | "yml" => return FormatType::Yaml,
|
||||||
"toml" => return FormatType::Toml,
|
"toml" => return FormatType::Toml,
|
||||||
|
"xml" => return FormatType::Xml,
|
||||||
|
"ini" => return FormatType::Ini,
|
||||||
|
"properties" => return FormatType::Properties,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +73,12 @@ pub fn detect_format(path: &Path, override_format: Option<String>) -> FormatType
|
|||||||
FormatType::Yaml
|
FormatType::Yaml
|
||||||
} else if file_name.ends_with(".toml") {
|
} else if file_name.ends_with(".toml") {
|
||||||
FormatType::Toml
|
FormatType::Toml
|
||||||
|
} else if file_name.ends_with(".xml") {
|
||||||
|
FormatType::Xml
|
||||||
|
} else if file_name.ends_with(".ini") {
|
||||||
|
FormatType::Ini
|
||||||
|
} else if file_name.ends_with(".properties") {
|
||||||
|
FormatType::Properties
|
||||||
} else {
|
} else {
|
||||||
FormatType::Env
|
FormatType::Env
|
||||||
}
|
}
|
||||||
@@ -76,5 +90,8 @@ pub fn get_handler(format: FormatType) -> Box<dyn FormatHandler> {
|
|||||||
FormatType::Json => Box::new(hierarchical::HierarchicalHandler::new(FormatType::Json)),
|
FormatType::Json => Box::new(hierarchical::HierarchicalHandler::new(FormatType::Json)),
|
||||||
FormatType::Yaml => Box::new(hierarchical::HierarchicalHandler::new(FormatType::Yaml)),
|
FormatType::Yaml => Box::new(hierarchical::HierarchicalHandler::new(FormatType::Yaml)),
|
||||||
FormatType::Toml => Box::new(hierarchical::HierarchicalHandler::new(FormatType::Toml)),
|
FormatType::Toml => Box::new(hierarchical::HierarchicalHandler::new(FormatType::Toml)),
|
||||||
|
FormatType::Xml => Box::new(hierarchical::HierarchicalHandler::new(FormatType::Xml)),
|
||||||
|
FormatType::Ini => Box::new(ini::IniHandler),
|
||||||
|
FormatType::Properties => Box::new(properties::PropertiesHandler),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
src/format/properties.rs
Normal file
129
src/format/properties.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
use super::{ConfigItem, FormatHandler, ItemStatus, ValueType};
|
||||||
|
use java_properties::{read, write};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, BufReader};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub struct PropertiesHandler;
|
||||||
|
|
||||||
|
impl FormatHandler for PropertiesHandler {
|
||||||
|
fn parse(&self, path: &Path) -> io::Result<Vec<ConfigItem>> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let props = read(reader)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||||
|
|
||||||
|
let mut vars = Vec::new();
|
||||||
|
let mut groups = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
for (path, value) in &props {
|
||||||
|
// Add groups based on dot notation
|
||||||
|
let parts: Vec<&str> = path.split('.').collect();
|
||||||
|
let mut current_path = String::new();
|
||||||
|
|
||||||
|
for i in 0..parts.len() - 1 {
|
||||||
|
if !current_path.is_empty() {
|
||||||
|
current_path.push('.');
|
||||||
|
}
|
||||||
|
current_path.push_str(parts[i]);
|
||||||
|
|
||||||
|
if groups.insert(current_path.clone()) {
|
||||||
|
vars.push(ConfigItem {
|
||||||
|
key: parts[i].to_string(),
|
||||||
|
path: current_path.clone(),
|
||||||
|
value: None,
|
||||||
|
template_value: None,
|
||||||
|
default_value: None,
|
||||||
|
depth: i,
|
||||||
|
is_group: true,
|
||||||
|
status: ItemStatus::Present,
|
||||||
|
value_type: ValueType::Null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vars.push(ConfigItem {
|
||||||
|
key: parts.last().unwrap().to_string(),
|
||||||
|
path: path.clone(),
|
||||||
|
value: Some(value.clone()),
|
||||||
|
template_value: Some(value.clone()),
|
||||||
|
default_value: Some(value.clone()),
|
||||||
|
depth: parts.len() - 1,
|
||||||
|
is_group: false,
|
||||||
|
status: ItemStatus::Present,
|
||||||
|
value_type: ValueType::String,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by path to keep it organized
|
||||||
|
vars.sort_by(|a, b| a.path.cmp(&b.path));
|
||||||
|
Ok(vars)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge(&self, path: &Path, vars: &mut Vec<ConfigItem>) -> io::Result<()> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing_vars = self.parse(path).unwrap_or_default();
|
||||||
|
|
||||||
|
for var in vars.iter_mut() {
|
||||||
|
if let Some(existing) = existing_vars.iter().find(|v| v.path == var.path) {
|
||||||
|
if var.value != existing.value {
|
||||||
|
var.value = existing.value.clone();
|
||||||
|
var.status = ItemStatus::Modified;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var.status = ItemStatus::MissingFromActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add items from active that are not in template
|
||||||
|
for existing in existing_vars {
|
||||||
|
if !vars.iter().any(|v| v.path == existing.path) {
|
||||||
|
let mut new_item = existing.clone();
|
||||||
|
new_item.status = ItemStatus::MissingFromTemplate;
|
||||||
|
new_item.template_value = None;
|
||||||
|
new_item.default_value = None;
|
||||||
|
vars.push(new_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(vars.sort_by(|a, b| a.path.cmp(&b.path)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self, path: &Path, vars: &[ConfigItem]) -> io::Result<()> {
|
||||||
|
let mut props = HashMap::new();
|
||||||
|
for var in vars {
|
||||||
|
if !var.is_group {
|
||||||
|
let val = var.value.as_deref()
|
||||||
|
.or(var.template_value.as_deref())
|
||||||
|
.unwrap_or("");
|
||||||
|
props.insert(var.path.clone(), val.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = File::create(path)?;
|
||||||
|
write(file, &props).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_properties() {
|
||||||
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
writeln!(file, "server.port=8080\ndatabase.host=localhost").unwrap();
|
||||||
|
|
||||||
|
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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,12 @@ pub const RULES: &[Rule] = &[
|
|||||||
Rule { template_suffix: ".template.yaml", active_suffix: ".yaml", is_exact_match: false },
|
Rule { template_suffix: ".template.yaml", active_suffix: ".yaml", is_exact_match: false },
|
||||||
Rule { template_suffix: ".example.toml", active_suffix: ".toml", is_exact_match: false },
|
Rule { template_suffix: ".example.toml", active_suffix: ".toml", is_exact_match: false },
|
||||||
Rule { template_suffix: ".template.toml", active_suffix: ".toml", is_exact_match: false },
|
Rule { template_suffix: ".template.toml", active_suffix: ".toml", is_exact_match: false },
|
||||||
|
Rule { template_suffix: ".example.xml", active_suffix: ".xml", is_exact_match: false },
|
||||||
|
Rule { template_suffix: ".template.xml", active_suffix: ".xml", is_exact_match: false },
|
||||||
|
Rule { template_suffix: ".example.ini", active_suffix: ".ini", is_exact_match: false },
|
||||||
|
Rule { template_suffix: ".template.ini", active_suffix: ".ini", is_exact_match: false },
|
||||||
|
Rule { template_suffix: ".example.properties", active_suffix: ".properties", is_exact_match: false },
|
||||||
|
Rule { template_suffix: ".template.properties", active_suffix: ".properties", is_exact_match: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const DEFAULT_CANDIDATES: &[&str] = &[
|
pub const DEFAULT_CANDIDATES: &[&str] = &[
|
||||||
|
|||||||
Reference in New Issue
Block a user