pub mod conf;
pub mod properties_parser;
pub mod ini_parser;
#[cfg(feature = "json")]
pub mod json_parser;
#[cfg(feature = "xml")]
pub mod xml_parser;
#[cfg(feature = "hcl")]
pub mod hcl_parser;
#[cfg(feature = "toml")]
pub mod toml_parser;
#[cfg(feature = "noml")]
pub mod noml_parser;
use crate::error::{Error, Result};
use crate::value::Value;
use std::path::Path;
pub fn parse_string(source: &str, format: Option<&str>) -> Result<Value> {
let detected_format = format.unwrap_or_else(|| detect_format(source));
match detected_format {
"conf" => conf::parse(source),
"properties" => properties_parser::parse(source),
"ini" => ini_parser::parse(source),
#[cfg(feature = "json")]
"json" => json_parser::parse(source),
#[cfg(feature = "xml")]
"xml" => xml_parser::parse(source),
#[cfg(feature = "hcl")]
"hcl" => hcl_parser::parse(source),
#[cfg(feature = "noml")]
"noml" => noml_parser::parse(source),
#[cfg(feature = "toml")]
"toml" => toml_parser::parse(source),
_ => {
#[cfg(not(feature = "json"))]
if detected_format == "json" {
return Err(Error::feature_not_enabled("json"));
}
#[cfg(not(feature = "xml"))]
if detected_format == "xml" {
return Err(Error::feature_not_enabled("xml"));
}
#[cfg(not(feature = "hcl"))]
if detected_format == "hcl" {
return Err(Error::feature_not_enabled("hcl"));
}
#[cfg(not(feature = "noml"))]
if detected_format == "noml" {
return Err(Error::feature_not_enabled("noml"));
}
#[cfg(not(feature = "toml"))]
if detected_format == "toml" {
return Err(Error::feature_not_enabled("toml"));
}
conf::parse(source)
}
}
}
pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<Value> {
let path = path.as_ref();
let content =
std::fs::read_to_string(path).map_err(|e| Error::io(path.display().to_string(), e))?;
let format = detect_format_from_path(path).or_else(|| Some(detect_format(&content)));
parse_string(&content, format)
}
#[cfg(feature = "async")]
pub async fn parse_file_async<P: AsRef<Path>>(path: P) -> Result<Value> {
let path = path.as_ref();
let content = tokio::fs::read_to_string(path)
.await
.map_err(|e| Error::io(path.display().to_string(), e))?;
let format = detect_format_from_path(path).or_else(|| Some(detect_format(&content)));
parse_string(&content, format)
}
pub fn detect_format_from_path(path: &Path) -> Option<&'static str> {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| match ext.to_lowercase().as_str() {
"conf" | "config" | "cfg" => "conf",
"properties" => "properties",
"ini" => "ini",
"toml" => "toml",
"json" => "json",
"noml" => "noml",
"xml" => "xml",
"hcl" | "tf" => "hcl", _ => "conf", })
}
pub fn detect_format(content: &str) -> &'static str {
let trimmed = content.trim();
if trimmed.starts_with('<') && contains_xml_features(content) {
return "xml";
}
if trimmed.starts_with('{') || trimmed.starts_with('[') {
return "json";
}
if contains_hcl_features(content) {
return "hcl";
}
if contains_noml_features(content) {
return "noml";
}
if contains_ini_features(content) {
return "ini";
}
if contains_properties_features(content) {
return "properties";
}
if contains_toml_features(content) {
return "toml";
}
"conf"
}
fn contains_noml_features(content: &str) -> bool {
content.contains("env(")
|| content.contains("include ")
|| content.contains("${")
|| content.contains("@size(")
|| content.contains("@duration(")
|| content.contains("@url(")
|| content.contains("@ip(")
}
fn contains_properties_features(content: &str) -> bool {
content.lines().any(|line| {
let trimmed = line.trim();
if trimmed.starts_with('!') {
return true;
}
if trimmed.contains("\\n") || trimmed.contains("\\t") || trimmed.contains("\\u") {
return true;
}
if trimmed.contains(':') && !trimmed.contains('=') && !trimmed.starts_with('#') {
return true;
}
false
})
}
fn contains_ini_features(content: &str) -> bool {
let mut has_section = false;
let mut has_ini_comment = false;
let mut has_key_value_in_section = false;
let mut in_section = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.contains('=') {
let section_content = &trimmed[1..trimmed.len() - 1];
if !section_content.contains('[') && !section_content.contains(']') {
has_section = true;
in_section = true;
continue;
}
}
if trimmed.starts_with(';') {
has_ini_comment = true;
}
if in_section
&& (trimmed.contains('=') || trimmed.contains(':'))
&& !trimmed.starts_with('#')
&& !trimmed.starts_with(';')
{
has_key_value_in_section = true;
}
}
has_section && has_key_value_in_section || has_ini_comment
}
fn contains_toml_features(content: &str) -> bool {
content.lines().any(|line| {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.contains('=') {
return true;
}
if trimmed.contains("T") && trimmed.contains("Z") {
return true;
}
false
})
}
fn contains_xml_features(content: &str) -> bool {
let trimmed = content.trim();
if trimmed.starts_with("<?xml") {
return true;
}
if trimmed.contains("</") {
return true;
}
if trimmed.contains("xmlns") {
return true;
}
if trimmed.contains("/>") {
return true;
}
let open_tags = trimmed.matches('<').count();
let close_tags = trimmed.matches('>').count();
open_tags > 0 && close_tags > 0 && open_tags <= close_tags
}
fn contains_hcl_features(content: &str) -> bool {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.contains(" \"") && trimmed.contains("\" {") {
return true;
}
if trimmed.starts_with("variable ") || trimmed.starts_with("output ") {
return true;
}
if trimmed.starts_with("resource ") || trimmed.starts_with("data ") {
return true;
}
if trimmed.starts_with("provider ") {
return true;
}
if trimmed.starts_with("terraform ") {
return true;
}
if trimmed.starts_with("module ") {
return true;
}
if trimmed.contains("${") && trimmed.contains("}") {
return true;
}
}
false
}