use crate::Result;
#[cfg(feature = "runtime-async-std")]
use async_std::{
io::prelude::*,
io::{
BufReader as ioBufReader, Cursor as ioCursor, Error as ioError,
ErrorKind as ioErrorKind,
},
};
#[cfg(all(feature = "runtime-async-std", not(target_arch = "wasm32")))]
use async_std::{fs::File as file, path::Path as ioPath};
#[cfg(feature = "runtime-tokio")]
use std::{io::Cursor as ioCursor, path::Path as ioPath};
#[cfg(feature = "runtime-tokio")]
use tokio::io::{
AsyncBufReadExt, AsyncReadExt, BufReader as ioBufReader, Error as ioError,
ErrorKind as ioErrorKind,
};
#[cfg(all(feature = "runtime-tokio", not(target_arch = "wasm32")))]
use tokio::fs::File as file;
use std::collections::HashMap;
const DEFAULT_SECTION: &str = "default";
const DEFAULT_COMMENT: &str = "#";
const DEFAULT_COMMENT_SEM: &str = ";";
const DEFAULT_MULTI_LINE_SEPARATOR: &str = "\\";
pub(crate) struct Config {
data: HashMap<String, HashMap<String, String>>,
}
impl Config {
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn from_file<P: AsRef<ioPath>>(p: P) -> Result<Self> {
let mut c = Config {
data: HashMap::new(),
};
c.parse(p).await?;
Ok(c)
}
pub(crate) async fn from_str<S: AsRef<str>>(s: S) -> Result<Self> {
let mut c = Config {
data: HashMap::new(),
};
c.parse_buffer(&mut ioBufReader::new(ioCursor::new(
s.as_ref().as_bytes(),
)))
.await?;
Ok(c)
}
#[cfg(not(target_arch = "wasm32"))]
async fn parse<P: AsRef<ioPath>>(&mut self, p: P) -> Result<()> {
let mut f = file::open(p).await?;
let mut c = Vec::new();
f.read_to_end(&mut c).await?;
let mut reader: ioBufReader<ioCursor<&[u8]>> =
ioBufReader::new(ioCursor::new(&c));
self.parse_buffer(&mut reader).await
}
async fn parse_buffer(
&mut self,
reader: &mut ioBufReader<ioCursor<&[u8]>>,
) -> Result<()> {
let mut section = String::new();
loop {
let mut line = String::new();
let bytes = reader.read_line(&mut line).await?;
if bytes == 0 {
break Ok(());
}
line = line.trim().to_string();
if line.is_empty()
|| line.starts_with(DEFAULT_COMMENT)
|| line.starts_with(DEFAULT_COMMENT_SEM)
{
continue;
} else if line.starts_with('[') && line.ends_with(']') {
section = line[1..line.len() - 1].to_string();
} else {
let mut next_section = String::new();
while line.ends_with(DEFAULT_MULTI_LINE_SEPARATOR) {
line = line[..line.len() - 1].trim_end().to_string();
let mut inner_line = String::new();
let inner_bytes = reader.read_line(&mut inner_line).await?;
if inner_bytes == 0 {
break;
}
let inner_line = inner_line.trim().to_string();
if inner_line.is_empty()
|| inner_line.starts_with(DEFAULT_COMMENT)
|| inner_line.starts_with(DEFAULT_COMMENT_SEM)
{
continue;
}
if inner_line.starts_with('[') && inner_line.ends_with(']')
{
next_section =
inner_line[1..inner_line.len() - 1].to_string();
} else {
line.push_str(&inner_line);
}
}
let option_val: Vec<&str> = line
.trim_end_matches(|c| {
char::is_whitespace(c)
|| char::to_string(&c)
== DEFAULT_MULTI_LINE_SEPARATOR
})
.splitn(2, '=')
.map(|e| e.trim())
.collect();
if option_val.len() != 2 {
return Err(ioError::new(
ioErrorKind::Other,
format!("parse content error, line={}", line),
)
.into());
}
self.add_config(
section.clone(),
option_val[0].to_string(),
option_val[1].to_string(),
);
if !next_section.is_empty() {
section = next_section;
}
}
}
}
pub(crate) fn add_config(
&mut self,
mut section: String,
option: String,
value: String,
) {
if section.is_empty() {
section = DEFAULT_SECTION.to_owned();
}
let section_value = self.data.entry(section).or_default();
let key_value = section_value.get_mut(&option);
match key_value {
Some(old_value) => {
*old_value = value;
}
None => {
section_value.insert(option, value);
}
}
}
pub fn get(&self, key: &str) -> Option<&str> {
let keys: Vec<String> =
key.to_lowercase().split("::").map(String::from).collect();
if keys.len() >= 2 {
let section = &keys[0];
let option = &keys[1];
self.data
.get(section)
.and_then(|m| m.get(option).map(|v| v.as_str()))
} else {
let section = DEFAULT_SECTION;
let option = &keys[0];
self.data
.get(section)
.and_then(|m| m.get(option).map(|v| v.as_str()))
}
}
#[allow(dead_code)]
pub(crate) fn set(&mut self, key: &str, value: &str) {
assert!(!key.is_empty(), "key can't be empty");
let keys: Vec<String> =
key.to_lowercase().split("::").map(String::from).collect();
if keys.len() >= 2 {
let section = &keys[0];
let option = &keys[1];
self.add_config(
section.to_owned(),
option.to_owned(),
value.to_owned(),
);
} else {
let section = DEFAULT_SECTION;
let option = &keys[0];
self.add_config(
section.to_owned(),
option.to_owned(),
value.to_owned(),
);
}
}
#[allow(dead_code)]
pub(crate) fn get_bool(&self, key: &str) -> Option<bool> {
self.get(key).and_then(|v| v.parse::<bool>().ok())
}
#[allow(dead_code)]
pub(crate) fn get_string(&self, key: &str) -> Option<String> {
self.get_str(key).map(|v| v.to_string())
}
pub(crate) fn get_str(&self, key: &str) -> Option<&str> {
self.get(key)
}
#[allow(dead_code)]
pub(crate) fn get_int(&self, key: &str) -> Option<i64> {
self.get(key).and_then(|v| v.parse::<i64>().ok())
}
#[allow(dead_code)]
pub(crate) fn get_float(&self, key: &str) -> Option<f64> {
self.get(key).and_then(|v| v.parse::<f64>().ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(
all(feature = "runtime-async-std", not(target_arch = "wasm32")),
async_std::test
)]
#[cfg_attr(
all(feature = "runtime-tokio", not(target_arch = "wasm32")),
tokio::test
)]
async fn test_get() {
let mut config =
Config::from_file("examples/testini.ini").await.unwrap();
assert_eq!(Some(true), config.get_bool("debug"));
assert_eq!(Some(64), config.get_int("math::math.i64"));
assert_eq!(Some(64.1), config.get_float("math::math.f64"));
assert_eq!(
Some("10.0.0.1".to_owned()),
config.get_string("mysql::mysql.master.host")
);
config.set("other::key1", "new test key");
assert_eq!(
Some("new test key".to_owned()),
config.get_string("other::key1")
);
config.set("other::key1", "test key");
assert_eq!(
Some("test key".to_owned()),
config.get_string("other::key1")
);
assert_eq!(
Some("r.sub==p.sub&&r.obj==p.obj".to_owned()),
config.get_string("multi1::name")
);
assert_eq!(
Some("r.sub==p.sub&&r.obj==p.obj".to_owned()),
config.get_string("multi2::name")
);
assert_eq!(
Some("r.sub==p.sub&&r.obj==p.obj".to_owned()),
config.get_string("multi3::name")
);
assert_eq!(Some("".to_owned()), config.get_string("multi4::name"));
assert_eq!(
Some("r.sub==p.sub&&r.obj==p.obj".to_owned()),
config.get_string("multi5::name")
);
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(
all(feature = "runtime-async-std", not(target_arch = "wasm32")),
async_std::test
)]
#[cfg_attr(
all(feature = "runtime-tokio", not(target_arch = "wasm32")),
tokio::test
)]
async fn test_from_text() {
let text: &str = r#"
# test config
debug = true
url = act.wiki
; redis config
[redis]
redis.key = push1,push2
; mysql config
[mysql]
mysql.dev.host = 127.0.0.1
mysql.dev.user = root
mysql.dev.pass = 123456
mysql.dev.db = test
mysql.master.host = 10.0.0.1
mysql.master.user = root
mysql.master.pass = 89dds)2$#d
mysql.master.db = act
; math config
[math]
math.i64 = 64
math.f64 = 64.1
"#;
let mut config = Config::from_str(text).await.unwrap();
assert_eq!(Some(true), config.get_bool("debug"));
assert_eq!(Some(64), config.get_int("math::math.i64"));
assert_eq!(Some(64.1), config.get_float("math::math.f64"));
assert_eq!(
Some("10.0.0.1".to_owned()),
config.get_string("mysql::mysql.master.host")
);
config.set("other::key1", "new test key");
assert_eq!(
Some("new test key".to_owned()),
config.get_string("other::key1")
);
config.set("other::key1", "test key");
assert_eq!(
Some("test key".to_owned()),
config.get_string("other::key1")
);
}
}