Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to serialize as a commented out line? #11

Closed
jmaibaum opened this issue Jul 8, 2019 · 4 comments
Closed

How to serialize as a commented out line? #11

jmaibaum opened this issue Jul 8, 2019 · 4 comments

Comments

@jmaibaum
Copy link

jmaibaum commented Jul 8, 2019

Hi,

I'm still pretty new to serde's concepts, so it might be that there is a very easy solution to my issue.

I want to be able to serialize some lines in my ini files as comments, for example if they contain a default value.

I.e. I might have some setting=100 which should be serialized as ;setting=100, if 100 happens to be the default value for this setting. These "commented out" default values are pretty common in configuration files as a form of self-documentation.

I see that the parser already has a Item::Comment variant that the writer seems to be able to serialize, but I can't find any serialize_comment (or similar) method anywhere and the writer's API is not public.

Any hints how to achieve my goal would bei highly appreciated. I don't mind if it involves a custom serialize_with attribute and function or a manually impl Serialize/r for T.

@arcnmx
Copy link
Owner

arcnmx commented Jul 8, 2019

This seems tricky, since I don't believe serde has any built-in concept of comments when serializing, and doesn't have a good way of interweaving metadata like a "this value is unchanged from default" flag to cross abstraction boundaries. serde_ini::Writer also isn't a trait so you can't really proxy it easily without buffering (serializing to vec/str, then parsing back out with serde_ini::Parser, then write back out again).

It's not pretty but your best bet might be something like:

use std::collections::HashMap;

trait DefaultValue<T: PartialEq> {
    const VALUE: T;
    const NAME: &'static str;
}

fn serialize_or_default<D: DefaultValue<T>, T: PartialEq + std::string::ToString, S: serde::Serializer>(value: &T, mut s: S) -> Result<S::Ok, S::Error> {
    let name = if value == &D::VALUE {
        format!(";{}", D::NAME)
    } else {
        D::NAME.to_owned()
    };
    serde::Serialize::serialize(
        &std::iter::once((name, value.to_string())).collect::<HashMap<_, _>>(),
        s
    )
}

struct DefaultSetting; impl DefaultValue<u32> for DefaultSetting { const NAME: &'static str = "setting"; const VALUE: u32 = 100; }
#[derive(Deserialize, Serialize, Debug)]
struct Data {
    #[serde(flatten, serialize_with = "serialize_or_default::<DefaultSetting, _, _>")]
    setting: u32,
}

assert_eq!(serde_ini::to_string(&Data {
    setting: 100
}).unwrap(), ";setting=100\r\n");
assert_eq!(serde_ini::to_string(&Data {
    setting: 99
}).unwrap(), "setting=99\r\n");

Not very optimal because flatten introduces buffering (plus ToString is needed due to #6, and HashMap isn't the best choice of an interim serializeable map) but it's probably the quickest and least invasive approach, just throw a macro around the DefaultValue impls to make them less verbose (DefaultSetting is used here as a marker type to hold the default and pass a parameter via serialize_with, so each field will need a unique empty struct like it - const generics would also work here instead).

@jmaibaum
Copy link
Author

jmaibaum commented Jul 9, 2019

Very interesting approach, thanks @arcnmx! It seems to be working good when serializing. Yet, when deserializing, the flatten attribute seems to be causing trouble:

Unwrapping creates a "can only flatten structs and maps" panic when adding an assert_eq!(serde_ini::from_str::<Data>("setting=100\r\n").unwrap(), Data { setting: 100 }); to your example code.

I have not yet fully grasped struct flattening in serde, but from quickly skimming through the docs, it seems to me as if I'd need some custom deserialize_with function as well which would be doing the inverse of what you did in serialize_or_default. Am I guessing right?

@arcnmx
Copy link
Owner

arcnmx commented Jul 9, 2019

Oh, sorry, I assumed flatten was a serialization-only attribute... You'd maybe just be better off using #[serde(into = ...)], there are a bunch of ways to do it but I guess something like this could work:

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(into = "DataWithDefaults")]
struct Data {
    setting: u32,
}

impl Default for Data {
    fn default() -> Self {
        Data {
            setting: 100,
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(transparent)]
struct DataWithDefaults(HashMap<String, String>);

fn convert(d: &Data) -> HashMap<&'static str, String> {
    // this could possibly be done lazy/messy using serde::de::IntoDeserializer or even like: serde_ini::from_str(&serde_ini::to_string(&d).unwrap()).unwrap()
    // consider an ordered map if that's important?
    std::iter::once(
        ("setting", d.setting.to_string())
    ).collect()
}

impl From<Data> for DataWithDefaults {
    fn from(d: Data) -> Self {
        let d = convert(&d);
        let defaults = convert(&Data::default());
        DataWithDefaults(
            d.into_iter()
                .map(|(k, v)| (
                    if defaults.get(&k) == Some(&v) { format!(";{}", k) } else { k.into() },
                    v
                )).collect()
        )
    }
}

assert_eq!(serde_ini::to_string(&Data {
    setting: 100
}).unwrap(), ";setting=100\r\n");
assert_eq!(serde_ini::to_string(&Data {
    setting: 99
}).unwrap(), "setting=99\r\n");
assert_eq!(serde_ini::from_str::<Data>("setting=100\r\n").unwrap(), Data {
    setting: 100
});

Part of the issue really just is that serde isn't very good at formatting data for human consumption. This works but is hacky, and if you want to do things like start adding like docs/comments to the file it gets messier. Making serde_ini::Writer generic might make things a little nicer to do that, hell a way around that would be to just use:

let mut writer = serde_ini::Writer::new(...);
for item in serde_ini::Parser::from_str(&serde_ini::to_string(data)?) {
    // match on item, check for keys with defaults, insert comments, etc. here
    writer.write(item.expect("we generated invalid ini?"))?
})

A generic Writer would allow you to avoid the to_string allocation but it's not like the above into hack is any better so...

@jmaibaum
Copy link
Author

jmaibaum commented Jul 10, 2019

Hi @arcnmx ,

thanks again for your kind help and all the example code!

Actually, I went for the #[serde(into = "...")] way now for my project. Yes, it is a little verbose, and it might not be the most efficient solution due to all the .to_string() allocations and the large HashMap (I actually used BTreeMap to have more control about the ordering of the serialization), but hey: it works! My convert function now has a large

[
    ("setting1", d.setting1.to_string()),
    ("setting2", d.setting2.to_string()),
    // ...
    ("settingn", d.settingn.to_string()),
]
.iter()
.cloned()
.collect()

block at the end, as I was not able to turn your idea to use serde::de::IntoDeserializer into code that the compiler accepted. I might try to reduce all the boilerplate with some custom macro_rules! later, but for now I'm kind of happy that it is possible to do what I want with what is there right now on stable and in the current version of serde_ini.

I admit that having a more powerful/generic serde_ini::Writer with the additions that you suggest above would be helpful in the long run, but for this small project your suggestions and hints solved my issue.

Thus I'm closing this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants