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

Support existing types with custom conversions #751

Open
proski opened this issue Jan 26, 2025 · 7 comments
Open

Support existing types with custom conversions #751

proski opened this issue Jan 26, 2025 · 7 comments

Comments

@proski
Copy link
Contributor

proski commented Jan 26, 2025

I'm trying to use fields of type std::time::Duration and initialize them with strings like "500 ms". It doesn't appear to be possible right now.

I converted my code to build.rs so I can explore all documented functions. The import_types! documentation is unclear and insufficient. Actually, there is no example of build.rs in the rustdoc documentation, but I found an example in the typify sources.

Only with_replacement lets me replace a generated type with the existing one (std::time::Duration). The documentation for with_conversion appears to promise that behavior, but a new type is always generated. Combinations of with_replacement and other with_* don't seem to work.

Unfortunately, with_replacement doesn't let me specify custom serde for the field. Look e.g. for #[serde(with = "humantime_serde")] at https://docs.rs/humantime-serde/1.1.1/humantime_serde/ - it's specified on the field, not on the whole struct.

I tried some things with x-rust-type without any success. In any case, I don't see it as a clean approach. It should be possible for the schema to be Rust-agnostic and still support readable strings for duration.

Likewise, I don't want to use numbers for duration. It's too easy to confuse seconds with milliseconds.

Currently supported formats ("uuid", "date", "ip" etc) all support conversion from string. The problem with std::time::Duration is that it's standard (i.e. many std functions use it) but cannot be constructed from a string. So a different mechanism is needed.

@proski
Copy link
Contributor Author

proski commented Jan 28, 2025

I confirm that adding #[serde(with = "humantime_serde"] before the Duration field is sufficient to enable support for parsing durations from strings.

I did it using regex in build.rs before writing the output to file.

    let re = Regex::new(r"\n *pub [a-z_][a-z0-9_]*: ::std::time::Duration,\n")
        .unwrap_or_else(|e| panic!("Cannot compile regex: {e}"));
    let output = re.replace_all(
        output.as_ref(),
        "\n    #[serde(with = \"humantime_serde\")]${0}",
    );

The fix in typify would probably involve adding a new TypeSpaceSettings method, let's call it with_serde:

    let mut settings = TypeSpaceSettings::default();
    settings
        .with_replacement("Duration", "::std::time::Duration", [].into_iter())
        .with_serde("Duration", "humantime_serde");

@ahl
Copy link
Collaborator

ahl commented Feb 2, 2025

To see if I understand correctly: you'd like to replace replace the type named Duration with ::std::time::Duration and you'd like to have a custom serialization and deserialization?

I think the former is well handled by the "replace" options, and it sounds like that's working sufficiently for you.

With regard to the latter, it sounds like you'd ideally like to have a #[serde(with .. )] attribute macro on every use of Duration e.g. where it's used a field in a struct. Is that right?

Would an alternative solution be to define your own newtype wrapper for Duration, define your custom serialization there, and use that for the replace imperative?

struct MyDuration(::std::time::Duration);

impl<'de> ::serde::Deserialize<'de> for MyDuration {
    fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
    where
        D: ::serde::Deserializer<'de>,
    {
        let s = <String>::deserialize(deserializer)?;
        todo!()
    }
}

// ...

@proski
Copy link
Contributor Author

proski commented Feb 3, 2025

I used DurationString from the duration-string originally. It's actually very close to what I want. I just had to add into() when invoking functions that expect Duration, such as socket.set_write_timeout().

It's still not ideal, as it exposes implementation details to the code. If my configuration has a field named send_timeout, it's not clean to use the type DurationString. Users of the field should not care that it was a string in the configuration. And DurationString is not actually a string.

A local type named e.g. crate::duration::Duration would probably look reasonable, but the users would still need to use into(), otherwise they would get a confusing error message that our Duration is not the Duration expected by the call.

Maybe I'm too picky, but the typify documentation made me think that there is a way to do type replacement and conversion even in the import_types! macro. I spent a lot of time trying various combinations, then I tried build.rs, looked at the typify code and finally came to the conclusion that I cannot do it. After all that effort, I'm staying with the regex post-processing in build.rs. But I'll be happy to ditch that regex with a future version of typify.

@ahl
Copy link
Collaborator

ahl commented Feb 3, 2025

Thanks for clarifying. The concept is interesting. One could imagine this as part of a type replacement. We already ask for the name of the existing type and some of its trait implementations; we could ask for serde attributes to apply... or even less one apply any attributes!

That said, I think we're unlikely to add this to typify--certainly not any time soon. In general, we try to keep typify as focused as we can. This might be the kind of augmentation that lives in a post-processing step--either as a regex or a more complex syn-based transformer.

@proski
Copy link
Contributor Author

proski commented Feb 4, 2025

OK, understood.

Maybe support for the "duration" format could be added? From my experience, duration is more common than absolute time, at least for configuration files.

@ahl
Copy link
Collaborator

ahl commented Feb 4, 2025

Are you thinking something like

{
    "type": "string",
    "format": "duration"
}

@proski
Copy link
Contributor Author

proski commented Feb 5, 2025

Yes. It should be deserialized from strings with units into std::time::Duration (no wrapping types). The ability to specify a custom parser would be nice but I can live without it if the default is adequate.

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