Thank you for the answer, I will try that.
Re: XY problem, you want to know more, I am still struggling with a problem I've described in the other question. I am trying to develop a two-phase config parsing system from TOML using Serde.
The key thing is the placeholders support. So any key instead of a=2
could be a="${foo}"
. At the same time it can be just a=2
too. The application that will use this system will also be separated in two parts, one where these variables at not known yet, and then they are known, so the Config
is "processed". I want to separate those two phases by type at compile time, so if the Config
was already processed, it has no placeholders and all values are fully resolved as plain types.
In the follow code I use Input
and Final
as "tags" for both forms of Config
.The key thing is the Field
, which is deserializable only in it's' Field<Input, T>
form, but not in the other. The process
function represents variable replacement, but here it is without params. Just imagine a HashMap
of values passed there.
Afaik without specializations as a feature of Rust is is more difficult to do, I settled to support just 3 field types for now. But anyway, it does not look great yet, but at least it works. The bound
requirement I need to put on every struct is a bit
#![allow(dead_code)]
use anyhow::Context;
use serde::{de, de::Visitor, Deserialize};
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use std::{
fmt::{Debug, Formatter},
marker::PhantomData,
};
#[derive(Debug, Clone)]
struct Final;
#[derive(Debug, Clone, Deserialize)]
struct Input;
#[derive(Debug, Clone)]
enum FieldData<T> {
Final(T),
Input(String),
}
#[derive(Clone)]
struct Field<S, T: Clone> {
data: FieldData<T>,
state: PhantomData<S>,
}
trait FieldConstructable {
fn from_string(s: &str) -> anyhow::Result<Field<Input, Self>>
where
Self: Clone;
fn from_i64(v: i64) -> anyhow::Result<Field<Input, Self>>
where
Self: Clone;
fn from_f64(v: f64) -> anyhow::Result<Field<Input, Self>>
where
Self: Clone;
}
impl FieldConstructable for String {
fn from_string(s: &str) -> anyhow::Result<Field<Input, Self>> {
Ok(Field::new_input(s))
}
fn from_i64(v: i64) -> anyhow::Result<Field<Input, Self>> {
Ok(Field::new_input(v.to_string()))
}
fn from_f64(v: f64) -> anyhow::Result<Field<Input, Self>> {
Ok(Field::new_input(v.to_string()))
}
}
impl FieldConstructable for f64 {
fn from_string(s: &str) -> anyhow::Result<Field<Input, Self>> {
Ok(Field::new_input(s))
}
fn from_i64(v: i64) -> anyhow::Result<Field<Input, Self>> {
Ok(Field::new_final(v as f64))
}
fn from_f64(v: f64) -> anyhow::Result<Field<Input, Self>> {
Ok(Field::new_final(v))
}
}
impl FieldConstructable for i64 {
fn from_string(s: &str) -> anyhow::Result<Field<Input, Self>> {
Ok(Field::new_input(s))
}
fn from_i64(v: i64) -> anyhow::Result<Field<Input, Self>> {
Ok(Field::new_final(v))
}
fn from_f64(_: f64) -> anyhow::Result<Field<Input, Self>> {
Err(anyhow::anyhow!("Expected integer, got float"))
}
}
struct FieldVisitor<T: Clone> {
pd: PhantomData<fn() -> T>,
}
impl<'de, T: Clone + FieldConstructable> Visitor<'de> for FieldVisitor<T> {
type Value = Field<Input, T>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(&format!(
"`{}` or a placeholder expession \"${{...}}\"",
std::any::type_name::<T>()
))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
FieldConstructable::from_string(&v).map_err(de::Error::custom)
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: de::Error,
{
FieldConstructable::from_string(&v).map_err(de::Error::custom)
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: de::Error,
{
FieldConstructable::from_i64(v).map_err(de::Error::custom)
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: de::Error,
{
FieldConstructable::from_f64(v).map_err(de::Error::custom)
}
}
impl<'de, T: Clone + FieldConstructable> Deserialize<'de> for Field<Input, T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(FieldVisitor { pd: PhantomData })
}
}
#[derive(thiserror::Error, Debug)]
enum ProcessingError {
#[error("unable process {field:?}, value={value:?}")]
CannotProcessField {
field: String,
value: String,
#[source]
source: Box<dyn std::error::Error + 'static + Sync + Send>,
},
}
impl<S, T: Default + Clone> Default for Field<S, T> {
fn default() -> Self {
Field {
data: FieldData::Final(Default::default()),
state: PhantomData,
}
}
}
impl<S, T: Debug + Clone> Debug for Field<S, T> {
fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
match &self.data {
FieldData::Final(p) => write!(f, "{:?}", p),
FieldData::Input(e) => write!(f, "expr({:?})", e),
}
}
}
impl<T: Clone> Field<Final, T> {
fn new(t: T) -> Self {
Self {
data: FieldData::Final(t),
state: PhantomData,
}
}
}
impl<T: Clone + FromStr> Field<Input, T> {
fn new_input<S: Into<String>>(expr: S) -> Self {
Self {
data: FieldData::Input(expr.into()),
state: PhantomData,
}
}
fn new_final<S: Into<T>>(t: S) -> Self {
Self {
data: FieldData::Final(t.into()),
state: PhantomData,
}
}
fn process(&self, name: &str) -> Result<Field<Final, T>, ProcessingError>
where
<T as FromStr>::Err: 'static + std::error::Error + Sync + Send,
{
match &self.data {
FieldData::Input(s) => match s.parse::<T>() {
Ok(t) => Ok(Field::new(t)),
Err(e) => Err(ProcessingError::CannotProcessField {
field: name.into(),
value: s.clone(),
source: Box::new(e),
}),
},
FieldData::Final(f) => Ok(Field::new(f.clone())),
}
}
}
impl<T: Clone> Deref for Field<Final, T> {
type Target = T;
fn deref(&self) -> &T {
match &self.data {
FieldData::Final(p) => p,
FieldData::Input(e) => {
panic!("Impossible input field {:?} is marked final", e);
}
}
}
}
impl<T: Clone> DerefMut for Field<Final, T> {
fn deref_mut(&mut self) -> &mut T {
match &mut self.data {
FieldData::Final(p) => p,
FieldData::Input(e) => {
panic!("Impossible input field {:?} is marked final", e);
}
}
}
}
// Configuration
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(bound(
deserialize = "Field<S, String>: Deserialize<'de>, Field<S, i64>: Deserialize<'de>, Field<S, f64>: Deserialize<'de>"
))]
pub struct Child<S: Clone> {
c: Field<S, f64>,
}
impl Child<Input> {
fn process(&self) -> anyhow::Result<Child<Final>> {
Ok(Child {
c: self.c.process("c")?,
})
}
}
impl From<String> for Field<Input, String> {
fn from(s: String) -> Self {
Field::new_input(s)
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(bound(
deserialize = "Field<S, String>: Deserialize<'de>, Field<S, i64>: Deserialize<'de>, Field<S, f64>: Deserialize<'de>"
))]
pub struct Config<S: Clone> {
a: Field<S, i64>,
#[serde(default = "x", bound = "Field<S, String>:From<String>")]
b: Field<S, String>,
c: Option<Field<S, String>>,
child: Child<S>,
}
fn x<S>() -> Field<S, String>
where
Field<S, String>: From<String>,
{
"default_b".to_owned().into()
}
impl Config<Input> {
fn process(&self) -> anyhow::Result<Config<Final>> {
Ok(Config {
a: self.a.process("a")?,
b: self.b.process("b")?,
c: self
.c
.as_ref()
.map(|c| c.process("c"))
.map_or(Ok(None), |r| r.map(Some))?,
child: self.child.process().context("Unable to process child")?,
})
}
}
fn main() -> anyhow::Result<()> {
let s = r#"
a = 2233
b = "hello"
c = "ccc"
[child]
c=2333
"#;
let c: Config<Input> = toml::from_str(&s)?;
println!("{:#?}", c);
let mut c: Config<Final> = c.process()?;
println!("{:#?}", c);
*c.a = 555;
println!("{:#?}", c);
Ok(())
}
Not a point of this questions, but any tips on how to approach this are welcome. Once most of it is moved to a separate module it is clean enough, except those bounds everywhere. Thanks!