added xml, properties and ini
All checks were successful
Version Check / check-version (pull_request) Successful in 3s
All checks were successful
Version Check / check-version (pull_request) Successful in 3s
This commit is contained in:
@@ -22,6 +22,8 @@ impl HierarchicalHandler {
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
|
||||
FormatType::Toml => toml::from_str(&content)
|
||||
.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!(),
|
||||
};
|
||||
Ok(value)
|
||||
@@ -47,12 +49,124 @@ impl HierarchicalHandler {
|
||||
));
|
||||
}
|
||||
}
|
||||
FormatType::Xml => json_to_xml(value),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
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>) {
|
||||
let path = if prefix.is_empty() {
|
||||
key_name.to_string()
|
||||
@@ -322,7 +436,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_flatten_unflatten() {
|
||||
fn test_json_flatten_unflatten() {
|
||||
let mut vars = Vec::new();
|
||||
let json = serde_json::json!({
|
||||
"services": {
|
||||
@@ -350,4 +464,129 @@ mod tests {
|
||||
assert!(unflattened_json.contains("\"8080:80\""));
|
||||
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 hierarchical;
|
||||
pub mod ini;
|
||||
pub mod properties;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ItemStatus {
|
||||
@@ -39,6 +41,9 @@ pub enum FormatType {
|
||||
Json,
|
||||
Yaml,
|
||||
Toml,
|
||||
Xml,
|
||||
Ini,
|
||||
Properties,
|
||||
}
|
||||
|
||||
pub trait FormatHandler {
|
||||
@@ -54,6 +59,9 @@ pub fn detect_format(path: &Path, override_format: Option<String>) -> FormatType
|
||||
"json" => return FormatType::Json,
|
||||
"yaml" | "yml" => return FormatType::Yaml,
|
||||
"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
|
||||
} else if file_name.ends_with(".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 {
|
||||
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::Yaml => Box::new(hierarchical::HierarchicalHandler::new(FormatType::Yaml)),
|
||||
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: ".example.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] = &[
|
||||
|
||||
Reference in New Issue
Block a user