pub mod config;
pub mod error;
#[doc(hidden)]
pub mod lexer;
pub(crate) mod numeric_array;
pub mod options;
#[doc(hidden)]
pub mod parser;
pub(crate) mod properties;
#[doc(hidden)]
pub mod resolver;
pub mod value;
mod value_factory;
#[cfg(feature = "serde")]
pub mod serde;
pub use config::{Config, Period};
pub use error::{ConfigError, HoconError, NotResolvedError, ParseError, ResolveError};
pub use options::{ParseOptions, ResolveOptions};
pub use value::{HoconValue, ScalarType, ScalarValue};
pub use value_factory::empty;
#[cfg(feature = "serde")]
pub use value_factory::from_map;
pub use lexer::{tokenize, Segment, SubstPayload, Token, TokenKind};
#[cfg(feature = "serde")]
pub use serde::DeserializeError;
use std::collections::HashMap;
use std::path::Path;
#[cfg(feature = "include-package")]
pub struct Parser {
registry: HashMap<(String, String), String>,
}
#[cfg(feature = "include-package")]
impl Default for Parser {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "include-package")]
impl Parser {
pub fn new() -> Self {
Parser {
registry: HashMap::new(),
}
}
pub fn register_package(
mut self,
identifier: impl Into<String>,
file: impl Into<String>,
content: impl Into<String>,
) -> Self {
let id = identifier.into();
let f = file.into();
let c = content.into();
if let Some(existing) = self.registry.get(&(id.clone(), f.clone())) {
if existing != &c {
panic!(
"hocon: conflicting content registered for package ({:?}, {:?}): \
different content already registered for this (identifier, file) pair",
id, f
);
}
} else {
self.registry.insert((id, f), c);
}
self
}
pub fn parse(self, input: &str) -> Result<Config, HoconError> {
self.parse_with_env(input, &std::env::vars().collect())
}
pub fn parse_file(self, path: impl AsRef<Path>) -> Result<Config, HoconError> {
self.parse_file_with_env(path, &std::env::vars().collect())
}
pub fn parse_with_env(
self,
input: &str,
env: &HashMap<String, String>,
) -> Result<Config, HoconError> {
self.parse_with_options(input, ParseOptions::defaults().with_env(env.clone()))
}
pub fn parse_file_with_env(
self,
path: impl AsRef<Path>,
env: &HashMap<String, String>,
) -> Result<Config, HoconError> {
self.parse_file_with_options(path, ParseOptions::defaults().with_env(env.clone()))
}
pub fn parse_with_options(self, input: &str, opts: ParseOptions) -> Result<Config, HoconError> {
let tokens = lexer::tokenize(input)?;
assert_non_empty_document(&tokens)?;
let ast = parser::parse_tokens(&tokens)?;
let env: HashMap<String, String> = opts.env.clone().unwrap_or_else(|| {
if opts.resolve_substitutions {
std::env::vars().collect()
} else {
HashMap::new()
}
});
let internal_opts = self.into_resolve_opts(env, opts.base_dir.clone());
if opts.resolve_substitutions {
let value = resolver::resolve(ast, &internal_opts)?;
match value {
HoconValue::Object(fields) => {
let mut cfg = Config::new(fields);
cfg.parse_base_dir = opts.base_dir;
cfg.origin_description = opts.origin_description;
Ok(cfg)
}
_ => Err(HoconError::Parse(ParseError {
message: "root must be an object".into(),
line: 1,
col: 1,
})),
}
} else {
let tree = resolver::build_tree(ast, &internal_opts)?;
Ok(Config::new_from_res_obj(
tree,
opts.base_dir,
opts.origin_description,
))
}
}
pub fn parse_file_with_options(
self,
path: impl AsRef<Path>,
opts: ParseOptions,
) -> Result<Config, HoconError> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)
.map_err(|e| std::io::Error::new(e.kind(), format!("{}: {}", path.display(), e)))?;
let base_dir = path.parent().map(|p| p.to_path_buf());
let opts = ParseOptions { base_dir, ..opts };
self.parse_with_options(&content, opts)
}
fn into_resolve_opts(
self,
env: HashMap<String, String>,
base_dir: Option<std::path::PathBuf>,
) -> resolver::InternalResolveOptions {
let mut opts = resolver::InternalResolveOptions::new(env);
if let Some(dir) = base_dir {
opts = opts.with_base_dir(dir);
}
opts.package_registry = std::sync::Arc::new(self.registry);
opts
}
}
pub fn parse(input: &str) -> Result<Config, HoconError> {
parse_with_env(input, &std::env::vars().collect())
}
pub fn parse_string_with_options(input: &str, opts: ParseOptions) -> Result<Config, HoconError> {
let tokens = lexer::tokenize(input)?;
assert_non_empty_document(&tokens)?;
let ast = parser::parse_tokens(&tokens)?;
let env: HashMap<String, String> = opts.env.clone().unwrap_or_else(|| {
if opts.resolve_substitutions {
std::env::vars().collect()
} else {
HashMap::new()
}
});
let mut internal_opts = resolver::InternalResolveOptions::new(env);
if let Some(ref bd) = opts.base_dir {
internal_opts = internal_opts.with_base_dir(bd.clone());
}
if opts.resolve_substitutions {
let value = resolver::resolve(ast, &internal_opts)?;
match value {
HoconValue::Object(fields) => {
let mut cfg = Config::new(fields);
cfg.parse_base_dir = opts.base_dir;
cfg.origin_description = opts.origin_description;
Ok(cfg)
}
_ => Err(HoconError::Parse(ParseError {
message: "root must be an object".into(),
line: 1,
col: 1,
})),
}
} else {
let tree = resolver::build_tree(ast, &internal_opts)?;
Ok(Config::new_from_res_obj(
tree,
opts.base_dir,
opts.origin_description,
))
}
}
pub fn parse_file_with_options<P: AsRef<Path>>(
path: P,
opts: ParseOptions,
) -> Result<Config, HoconError> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)
.map_err(|e| std::io::Error::new(e.kind(), format!("{}: {}", path.display(), e)))?;
let base_dir = path.parent().map(|p| p.to_path_buf());
let opts = ParseOptions { base_dir, ..opts };
parse_string_with_options(&content, opts)
}
pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<Config, HoconError> {
parse_file_with_env(path, &std::env::vars().collect())
}
pub fn parse_file_with_env<P: AsRef<Path>>(
path: P,
env: &HashMap<String, String>,
) -> Result<Config, HoconError> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)
.map_err(|e| std::io::Error::new(e.kind(), format!("{}: {}", path.display(), e)))?;
let tokens = lexer::tokenize(&content)?;
assert_non_empty_document(&tokens)?;
let ast = parser::parse_tokens(&tokens)?;
let mut opts = resolver::InternalResolveOptions::new(env.clone());
if let Some(dir) = path.parent() {
opts = opts.with_base_dir(dir.to_path_buf());
}
let value = resolver::resolve(ast, &opts)?;
match value {
HoconValue::Object(fields) => Ok(Config::new(fields)),
_ => Err(HoconError::Parse(ParseError {
message: "root must be an object".into(),
line: 1,
col: 1,
})),
}
}
pub fn parse_with_env(input: &str, env: &HashMap<String, String>) -> Result<Config, HoconError> {
let tokens = lexer::tokenize(input)?;
assert_non_empty_document(&tokens)?;
let ast = parser::parse_tokens(&tokens)?;
let opts = resolver::InternalResolveOptions::new(env.clone());
let value = resolver::resolve(ast, &opts)?;
match value {
HoconValue::Object(fields) => Ok(Config::new(fields)),
_ => Err(HoconError::Parse(ParseError {
message: "root must be an object".into(),
line: 1,
col: 1,
})),
}
}
#[doc(hidden)]
pub fn _render_json_for_test(config: &Config) -> String {
use crate::value::HoconValue;
use std::fmt::Write;
fn render_value(val: &HoconValue, out: &mut String) {
match val {
HoconValue::Scalar(sv) => {
use crate::value::ScalarType;
match sv.value_type {
ScalarType::Null => out.push_str("null"),
ScalarType::Boolean => out.push_str(&sv.raw),
ScalarType::Number => out.push_str(&sv.raw),
ScalarType::String => {
let escaped = sv
.raw
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
let _ = write!(out, "\"{}\"", escaped);
}
}
}
HoconValue::Object(map) => {
out.push('{');
let mut keys: Vec<&str> = map.keys().map(|s| s.as_str()).collect();
keys.sort_unstable();
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
let _ = write!(out, "\"{}\":", k);
render_value(map.get(*k).unwrap(), out);
}
out.push('}');
}
HoconValue::Array(arr) => {
out.push('[');
for (i, v) in arr.iter().enumerate() {
if i > 0 {
out.push(',');
}
render_value(v, out);
}
out.push(']');
}
HoconValue::Placeholder(pv) => {
let _ = write!(out, "\"<unresolved:{}>\"", pv.path);
}
}
}
let mut out = String::from("{");
let mut keys: Vec<&str> = config.root.keys().map(|s| s.as_str()).collect();
keys.sort_unstable();
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
let _ = write!(out, "\"{}\":", k);
render_value(config.root.get(*k).unwrap(), &mut out);
}
out.push('}');
out
}
fn assert_non_empty_document(tokens: &[lexer::Token]) -> Result<(), HoconError> {
let has_content = tokens
.iter()
.any(|t| !matches!(t.kind, lexer::TokenKind::Newline | lexer::TokenKind::Eof));
if !has_content {
return Err(HoconError::Parse(ParseError {
message: "empty file is not a valid HOCON document (HOCON.md L130)".into(),
line: 1,
col: 1,
}));
}
Ok(())
}