anixe / doku

fn(Code) -> Docs
MIT License
80 stars 5 forks source link

`to_json_val` results in type information instead of value when using `skip_serializing` #16

Closed arctic-alpaca closed 2 years ago

arctic-alpaca commented 2 years ago

Hi,

when creating a JSON representation of an initialized struct via doku::to_json_val(&Config::default());, fields that are marked with #[serde(skip_serializing)] are set to their respective type and not, as expected, to the corresponding value.

use doku::Document;
use serde::Serialize;

#[derive(Serialize, Document)]
struct Config {
    /// Database's host
    db_host: String,
    #[serde(skip_serializing)]
    skip_ser: String,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            db_host: "localhost".to_string(),
            skip_ser: "skip_ser".to_string(),
        }
    }
}

fn main() {
    let doc = doku::to_json_val(&Config::default());
    // passes
    assert_eq!(
        r#"{
  // Database's host
  "db_host": "localhost",
  "skip_ser": "string"
}"#,
        doc
    );

    // fails
    assert_eq!(
        r#"{
  // Database's host
  "db_host": "localhost",
  "skip_ser": "skip_ser"
}"#,
        doc
    );
}
Patryk27 commented 2 years ago

Hi,

When using to_json_val(), Doku relies on Serde to provide values for fields - since you're using skip_serializing, Serde doesn't provide those values and so unfortunately Doku has no way of knowing/discovering them.

You could instruct Doku to ignore those fields though:

let doc = doku::json::Printer::default()
    .with_visibility(doku::Visibility::SerializableOnly)
    .with_value(&Config::default())
    .print(&Config::ty());

This should return a document without the skip_ser field present at all 🙂

arctic-alpaca commented 2 years ago

My use-case is a service that expects a JSON input to deserialize into a struct and then serialize that struct to pass to another service. Parts of the struct should not be serialized and passed to the other service but used otherwise. To document this service, fields with #[serde(skip_serializing)] need to present in the documentation since they are expected when deserializing the input. Ideally the documentation would contain example values which are also used in tests to verify that the provided documentation always works.

The easiest approach I found would be to use #[serde(skip_serializing)]. Other approaches I think would work:

  1. Use #[doku(example = "skip_ser")] with doku::to_json::<Config>(). This would work but create lots of example annotation which would need to be maintained in addition to example structs used in tests.
  2. Same as 1 but use Doku to create a JSON documentation of a struct, then use the serde_json crate to deserialize this into a struct for testing to avoid having to maintain example annotation and implementation. This prevents the use of comments in JSON since serde_json doesn't support comments.
  3. Create two separate structs, one to deserialize the input into, then convert into the second struct and use that with #[serde(skip_serializing)]. Double the structs for every input doesn't seem practical.

Building on option 1, would it be possible to create an initialized struct from the example annotations? Something like build_example_struct::<Config>() -> Config. This would allow me to test the service with the documented example values without having to maintain a separate implementation for tests.

Patryk27 commented 2 years ago

Ok, I think I understand your use case now - I have my own two suggestions:

  1. How about if for each input instead of providing just one documentation, you provided two: one describing how the input is deserialized, and another for how the inputs gets deserialized?

    Since you're already using skip_serializing, you wouldn't have to actually duplicate the structs you already have - the only thing that'd change is that for each input struct you'd call Doku twice: once with doku::Visibility::SerializableOnly, once with doku::Visibility::DeserializableOnly, and then in the documentation you'd say this is Foo's input: ..., this is Foo's output: ....

    This should nicely integrate with your JSON test-files, since for this is Foo's input you could load some example input file, and vice versa for the output docs.

  2. I could implement some sort of a value-merging, so that you would do:

    let config_input = load_json("tests/config.input.json");
    let config_output = load_json("tests/config.output.json");
    
    let doc = doku::json::Printer::default()
        .with_values(&[config_input, config_output])
        .print(&Config::ty());

    ... which would try reading values first from config_input, filling out the missing ones from config_output.

As for using examples (so your first and third ideas), I think this might become troublesome in the long run, since Doku doesn't allow to provide different examples based on the context - say, given:

struct Foo {
    #[doku(example = "something")]
    value: String,
}

struct Bar {
    foo: Foo,
}

struct Zar {
    foo: Foo,
}

... it's impossible to provide a different example for Bar.foo.value & Zar.foo.value, which depending on how your API looks like, might affect your documentation's usability a bit.

arctic-alpaca commented 2 years ago

Thanks a lot for your suggestions! Your second idea led me to play around with the Printer a bit more and I found a workaround to deal with #[serde(skip_serializing)] that works perfectly for my usecase. Since you make it easy to skip comments when printing, I can just use point 2 from my previous post. This results in this code:

use doku::Document;
use doku::json::*;
use serde::Serialize;
use serde::Deserialize;

#[derive(Serialize, Deserialize,  Document, Debug)]
struct Config {
    /// Database's host
    #[doku(example = "test")]
    db_host: String,

    #[doku(example = "skip_ser")]
    #[serde(skip_serializing)]
    skip_ser: String,
}

fn main() {
    let doc_comments = Printer::default().print(&Config::ty());
    let mut formatting = Formatting::default();
    formatting.doc_comments = DocComments::Hidden;
    let doc_example = Printer::default().with_formatting(&formatting).print(&Config::ty());
    let doc_example_deserialized = serde_json::from_str::<Config>(&doc_example).unwrap();
    println!("doku with comments: \n {}", doc_comments);
    println!("doku: \n {}", doc_example);
    println!("deserialized struct: \n {:#?}", doc_example_deserialized);
}

Luckily my API currently doesn't need examples based on the context in which a struct is used. I'll keep this in mind in my design going forward.

Thank you for your help!