use std::ops::Range;
use std::path::Path;
use std::sync::Arc;
pub use crate::error::FormatError;
#[cfg_attr(
not(all(
feature = "json",
feature = "yaml",
feature = "toml",
feature = "hcl",
feature = "ini",
feature = "xml",
feature = "properties"
)),
allow(dead_code)
)]
mod comment;
mod splice;
mod text;
#[cfg(feature = "csv")]
mod csv;
#[cfg(feature = "dotenv")]
mod dotenv;
#[cfg(feature = "hcl")]
mod hcl;
#[cfg(any(feature = "ini", feature = "dotenv"))]
#[cfg_attr(not(feature = "ini"), allow(dead_code))]
mod ini;
#[cfg(feature = "json")]
mod json;
#[cfg(feature = "plist")]
mod plist;
#[cfg(feature = "properties")]
mod properties;
#[cfg(feature = "toml")]
mod toml;
#[cfg(feature = "xml")]
mod xml;
#[cfg(feature = "yaml")]
mod yaml;
pub use splice::Splicer;
pub use text::Text;
#[cfg(feature = "csv")]
pub use self::csv::Csv;
#[cfg(feature = "toml")]
pub use self::toml::Toml;
#[cfg(feature = "dotenv")]
pub use dotenv::Dotenv;
#[cfg(feature = "hcl")]
pub use hcl::Hcl;
#[cfg(feature = "ini")]
pub use ini::Ini;
#[cfg(feature = "json")]
pub use json::{Json, JsonLines};
#[cfg(feature = "plist")]
pub use plist::BinaryPlist;
#[cfg(feature = "properties")]
pub use properties::Properties;
#[cfg(feature = "xml")]
pub use xml::Xml;
#[cfg(feature = "yaml")]
pub use yaml::Yaml;
pub trait Format: Send + Sync {
fn name(&self) -> &str;
fn extensions(&self) -> &[&str] {
&[]
}
fn file_names(&self) -> &[&str] {
&[]
}
fn matches_file_name(&self, name: &str) -> bool {
self.file_names().contains(&name)
}
fn sniff(&self, _input: &[u8]) -> bool {
false
}
fn rewrite(&self, input: &[u8], visitor: &mut dyn LeafVisitor) -> Result<Vec<u8>, FormatError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LeafKind {
#[default]
Value,
Comment,
}
#[derive(Debug, Clone, Copy)]
pub struct Leaf<'a> {
pub value: &'a str,
pub key: Option<&'a str>,
pub offset: Option<usize>,
pub kind: LeafKind,
}
impl<'a> Leaf<'a> {
pub fn new(value: &'a str) -> Self {
Self {
value,
key: None,
offset: None,
kind: LeafKind::Value,
}
}
pub fn comment(value: &'a str) -> Self {
Self {
kind: LeafKind::Comment,
..Self::new(value)
}
}
pub fn with_key(mut self, key: Option<&'a str>) -> Self {
self.key = key;
self
}
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
}
#[derive(Debug, Default, Clone)]
pub struct Object<'a> {
fields: Vec<(&'a str, Option<&'a str>)>,
}
impl<'a> Object<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, key: &'a str, string_value: Option<&'a str>) {
self.fields.push((key, string_value));
}
pub fn keys(&self) -> impl Iterator<Item = &'a str> + '_ {
self.fields.iter().map(|(k, _)| *k)
}
pub fn get_str(&self, key: &str) -> Option<&'a str> {
self.fields
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.and_then(|(_, v)| *v)
}
}
impl<'a> FromIterator<(&'a str, Option<&'a str>)> for Object<'a> {
fn from_iter<T: IntoIterator<Item = (&'a str, Option<&'a str>)>>(iter: T) -> Self {
Self {
fields: iter.into_iter().collect(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Container<'a, 'b> {
Object(&'b Object<'a>),
Array,
}
pub trait LeafVisitor {
fn enter(&mut self, key: Option<&str>, container: Container<'_, '_>);
fn exit(&mut self);
fn leaf(&mut self, leaf: &Leaf<'_>) -> Option<Replacement>;
fn wants_comments(&self) -> bool {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Replacement {
pub value: String,
pub edits: Vec<Edit>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Edit {
pub range: Range<usize>,
pub text: String,
}
pub const ALL_NAMES: &[&str] = &[
"json",
"jsonl",
"yaml",
"toml",
"xml",
"hcl",
"ini",
"dotenv",
"properties",
"csv",
"tsv",
"psv",
"bplist",
"text",
];
pub fn builtin() -> Vec<Arc<dyn Format>> {
#[allow(unused_mut)]
let mut formats: Vec<Arc<dyn Format>> = vec![Arc::new(Text)];
#[cfg(feature = "json")]
{
formats.push(Arc::new(Json));
formats.push(Arc::new(JsonLines));
}
#[cfg(feature = "yaml")]
formats.push(Arc::new(Yaml));
#[cfg(feature = "toml")]
formats.push(Arc::new(Toml));
#[cfg(feature = "xml")]
formats.push(Arc::new(Xml));
#[cfg(feature = "hcl")]
formats.push(Arc::new(Hcl));
#[cfg(feature = "ini")]
formats.push(Arc::new(Ini));
#[cfg(feature = "dotenv")]
formats.push(Arc::new(Dotenv));
#[cfg(feature = "properties")]
formats.push(Arc::new(Properties));
#[cfg(feature = "csv")]
{
formats.push(Arc::new(Csv::comma()));
formats.push(Arc::new(Csv::tab()));
formats.push(Arc::new(Csv::pipe()));
}
#[cfg(feature = "plist")]
formats.push(Arc::new(BinaryPlist));
formats
}
#[derive(Clone)]
pub struct FormatRegistry {
formats: Vec<Arc<dyn Format>>,
}
impl Default for FormatRegistry {
fn default() -> Self {
Self { formats: builtin() }
}
}
impl FormatRegistry {
pub fn text_only() -> Self {
Self {
formats: vec![Arc::new(Text)],
}
}
pub fn register(&mut self, format: Arc<dyn Format>) {
self.formats.retain(|f| f.name() != format.name());
self.formats.push(format);
}
pub fn get(&self, name: &str) -> Option<Arc<dyn Format>> {
self.formats
.iter()
.rev()
.find(|f| f.name() == name)
.cloned()
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.formats.iter().map(|f| f.name())
}
pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn Format>> {
self.formats.iter()
}
pub fn for_path(&self, path: &Path) -> Option<Arc<dyn Format>> {
let name = path.file_name()?.to_str()?.to_ascii_lowercase();
if let Some(f) = self
.formats
.iter()
.rev()
.find(|f| f.matches_file_name(&name))
{
return Some(f.clone());
}
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
self.formats
.iter()
.rev()
.find(|f| f.extensions().contains(&ext.as_str()))
.cloned()
}
pub fn sniff(&self, input: &[u8]) -> Option<Arc<dyn Format>> {
self.formats.iter().find(|f| f.sniff(input)).cloned()
}
pub fn text(&self) -> Arc<dyn Format> {
self.get(Text::NAME).unwrap_or_else(|| Arc::new(Text))
}
}
pub(crate) fn apply_edits(value: &str, edits: &[Edit]) -> String {
let mut out = String::with_capacity(value.len());
let mut prev = 0;
for edit in edits {
out.push_str(&value[prev..edit.range.start]);
out.push_str(&edit.text);
prev = edit.range.end;
}
out.push_str(&value[prev..]);
out
}