Skip to content

Commit

Permalink
Merge pull request #22 from fizyr/toml
Browse files Browse the repository at this point in the history
Add optional support for TOML data.
  • Loading branch information
de-vri-es authored Jun 27, 2024
2 parents 26213d2 + 92f701f commit 4e1d60d
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 6 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --all-targets --all-features --color=always
args: --workspace --all-targets --features toml,yaml --color=always
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace --all-targets --all-features --color=always
args: --workspace --all-targets --features toml,yaml --color=always
- name: Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --all-targets --all-features
args: --workspace --all-targets --features toml,yaml
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Unreleased
- [add][minor] Add support for substitution in all string values of TOML data.

# Version 0.3.2 - 2024-06-25
- [add][minor] Allow re-use of parsed templates with `Template`, `TemplateBuf`, `ByteTemplate` and `ByteTemplateBuf`.

Expand Down
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ categories = ["template-engine", "value-formatting"]
edition = "2021"

[features]
toml = ["dep:serde", "dep:toml"]
yaml = ["dep:serde", "dep:serde_yaml"]
preserve-order = ["toml?/preserve_order"]
doc-cfg = []

[dependencies]
memchr = "2.4.1"
serde = { version = "1.0.0", optional = true }
serde_yaml = { version = "0.9.13", optional = true }
toml = { version = "0.8.14", optional = true }
unicode-width = "0.1.9"

[dev-dependencies]
assert2 = "0.3.6"
subst = { path = ".", features = ["yaml"] }
subst = { path = ".", features = ["toml", "yaml"] }
serde = { version = "1.0.0", features = ["derive"] }

[package.metadata.docs.rs]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Shell-like variable substitution for strings and byte strings.
* Long format: `"Hello ${name}!"`
* Default values: `"Hello ${name:person}!"`
* Recursive substitution in default values: `"${XDG_CONFIG_HOME:$HOME/.config}/my-app/config.toml"`
* Perform substitution on all string values in YAML data (optional, requires the `yaml` feature).
* Perform substitution on all string values in TOML or YAML data (optional, requires the `toml` or `yaml` feature).

Variable names can consist of alphanumeric characters and underscores.
They are allowed to start with numbers.
Expand Down
9 changes: 8 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! * Long format: `"Hello ${name}!"`
//! * Default values: `"Hello ${name:person}!"`
//! * Recursive substitution in default values: `"${XDG_CONFIG_HOME:$HOME/.config}/my-app/config.toml"`
//! * Perform substitution on all string values in YAML data (optional, requires the `yaml` feature).
//! * Perform substitution on all string values in TOML or YAML data (optional, requires the `toml` or `yaml` feature).
//!
//! Variable names can consist of alphanumeric characters and underscores.
//! They are allowed to start with numbers.
Expand Down Expand Up @@ -79,7 +79,9 @@
//! # Ok(())
//! # }
//! ```

#![warn(missing_docs, missing_debug_implementations)]
#![cfg_attr(feature = "doc-cfg", feature(doc_cfg))]

pub mod error;
pub use error::Error;
Expand All @@ -91,8 +93,13 @@ mod template;
pub use template::*;

#[cfg(feature = "yaml")]
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "yaml")))]
pub mod yaml;

#[cfg(feature = "toml")]
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "toml")))]
pub mod toml;

/// Substitute variables in a string.
///
/// Variables have the form `$NAME`, `${NAME}` or `${NAME:default}`.
Expand Down
228 changes: 228 additions & 0 deletions src/toml.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
//! Support for variable substitution in TOML data.

use serde::de::DeserializeOwned;

use crate::VariableMap;

/// Parse a struct from TOML data, after performing variable substitution on string values.
///
/// This function first parses the data into a [`toml::Value`],
/// then performs variable substitution on all string values,
/// and then parses it further into the desired type.
pub fn from_slice<'a, T: DeserializeOwned, M>(data: &[u8], variables: &'a M) -> Result<T, Error>
where
M: VariableMap<'a> + ?Sized,
M::Value: AsRef<str>,
{
from_str(std::str::from_utf8(data)?, variables)
}

/// Parse a struct from TOML data, after performing variable substitution on string values.
///
/// This function first parses the data into a [`toml::Value`],
/// then performs variable substitution on all string values,
/// and then parses it further into the desired type.
pub fn from_str<'a, T: DeserializeOwned, M>(data: &str, variables: &'a M) -> Result<T, Error>
where
M: VariableMap<'a> + ?Sized,
M::Value: AsRef<str>,
{
let mut value: toml::Value = toml::from_str(data)?;
substitute_string_values(&mut value, variables)?;
Ok(T::deserialize(value)?)
}

/// Perform variable substitution on string values of a TOML value.
pub fn substitute_string_values<'a, M>(value: &mut toml::Value, variables: &'a M) -> Result<(), crate::Error>
where
M: VariableMap<'a> + ?Sized,
M::Value: AsRef<str>,
{
visit_string_values(value, |value| {
*value = crate::substitute(value.as_str(), variables)?;
Ok(())
})
}

/// Error for parsing TOML with variable substitution.
#[derive(Debug)]
pub enum Error {
/// The input contains invalid UTF-8.
InvalidUtf8(std::str::Utf8Error),

/// An error occurred while parsing TOML.
Toml(toml::de::Error),

/// An error occurred while performing variable substitution.
Subst(crate::Error),
}

impl From<std::str::Utf8Error> for Error {
#[inline]
fn from(other: std::str::Utf8Error) -> Self {
Self::InvalidUtf8(other)
}
}

impl From<toml::de::Error> for Error {
#[inline]
fn from(other: toml::de::Error) -> Self {
Self::Toml(other)
}
}

impl From<crate::Error> for Error {
#[inline]
fn from(other: crate::Error) -> Self {
Self::Subst(other)
}
}

impl std::error::Error for Error {}

impl std::fmt::Display for Error {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidUtf8(e) => std::fmt::Display::fmt(e, f),
Self::Toml(e) => std::fmt::Display::fmt(e, f),
Self::Subst(e) => std::fmt::Display::fmt(e, f),
}
}
}

/// Recursively apply a function to all string values in a TOML value.
fn visit_string_values<F, E>(value: &mut toml::Value, fun: F) -> Result<(), E>
where
F: Copy + Fn(&mut String) -> Result<(), E>,
{
match value {
toml::Value::Boolean(_) => Ok(()),
toml::Value::Integer(_) => Ok(()),
toml::Value::Float(_) => Ok(()),
toml::Value::Datetime(_) => Ok(()),
toml::Value::String(val) => fun(val),
toml::Value::Array(seq) => {
for value in seq {
visit_string_values(value, fun)?;
}
Ok(())
},
toml::Value::Table(map) => {
for (_key, value) in map.iter_mut() {
visit_string_values(value, fun)?;
}
Ok(())
},
}
}

#[cfg(test)]
#[rustfmt::skip]
mod test {
use std::collections::HashMap;

use super::*;
use assert2::{assert, let_assert};

#[test]
fn test_from_str() {
#[derive(Debug, serde::Deserialize)]
struct Struct {
bar: String,
baz: String,
}

let mut variables = HashMap::new();
variables.insert("bar", "aap");
variables.insert("baz", "noot");
#[rustfmt::skip]
let_assert!(Ok(parsed) = from_str(
concat!(
"bar = \"$bar\"\n",
"baz = \"$baz/with/stuff\"\n",
),
&variables,
));

let parsed: Struct = parsed;
assert!(parsed.bar == "aap");
assert!(parsed.baz == "noot/with/stuff");
}

#[test]
fn test_from_str_no_substitution() {
#[derive(Debug, serde::Deserialize)]
struct Struct {
bar: String,
baz: String,
}

let mut variables = HashMap::new();
variables.insert("bar", "aap");
variables.insert("baz", "noot");
#[rustfmt::skip]
let_assert!(Ok(parsed) = from_str(
concat!(
"bar = \"aap\"\n",
"baz = \"noot/with/stuff\"\n",
),
&crate::NoSubstitution,
));

let parsed: Struct = parsed;
assert!(parsed.bar == "aap");
assert!(parsed.baz == "noot/with/stuff");
}

#[test]
fn test_toml_in_var_is_not_parsed() {
#[derive(Debug, serde::Deserialize)]
struct Struct {
bar: String,
baz: String,
}

let mut variables = HashMap::new();
variables.insert("bar", "aap\nbaz = \"mies\"");
variables.insert("baz", "noot");
#[rustfmt::skip]
let_assert!(Ok(parsed) = from_str(
concat!(
"bar = \"$bar\"\n",
"baz = \"$baz\"\n",
),
&variables,
));

let parsed: Struct = parsed;
assert!(parsed.bar == "aap\nbaz = \"mies\"");
assert!(parsed.baz == "noot");
}

#[test]
fn test_dyn_variable_map() {
#[derive(Debug, serde::Deserialize)]
struct Struct {
bar: String,
baz: String,
}

let mut variables = HashMap::new();
variables.insert("bar", "aap");
variables.insert("baz", "noot");
let variables: &dyn VariableMap<Value = &&str> = &variables;
#[rustfmt::skip]
let_assert!(Ok(parsed) = from_str(
concat!(
"bar = \"$bar\"\n",
"baz = \"$baz/with/stuff\"\n",
),
variables,
));

let parsed: Struct = parsed;
assert!(parsed.bar == "aap");
assert!(parsed.baz == "noot/with/stuff");
}
}

0 comments on commit 4e1d60d

Please sign in to comment.