[solved] Rust idiomatic way to handle different versions of a server and the responses

Hi,

I'm working on gerrit-rust a cli and api to speak with a gerrit server via a json protocol. Gerrit returns a json object and I deserialize it to some structs.
Gerrits protocol evolves between release slightly and I have to add code to handle this version differences. My first approach was to support only the oldest version, but now I want to do it right :slight_smile:.

A good example is the RevisionInfo entity where the fields names, meanings and other things like "optional" between versions differ. I have added a short description of RevisionInfo from both version on this topic.

My Question is now, how to write code to handle the return values of different version of the gerrit server?


Version 2.9

(excerpt from 2.9 documentation)

  • draft: Whether the patch set is a draft.
  • has_draft_comments: Whether the patch set has one or more draft comments by the calling user. Only set if draft comments is requested.
  • _number: The patch set number.
  • fetch: Information about how to fetch this patch set. The fetch information is provided as a map that maps the protocol name ("git", "http", "ssh") to FetchInfo entities.
  • commit: The commit of the patch set as CommitInfo entity.
  • files: The files of the patch set as a map that maps the file names to FileInfo entities.
  • actions: Actions the caller might be able to perform on this revision. The information is a map of view name to ActionInfo entities.

version 2.13

(excerpt from 2.13 documentation)

  • draft: Whether the patch set is a draft.
  • kind: The change kind. Valid values are REWORK, TRIVIAL_REBASE, MERGE_FIRST_PARENT_UPDATE, NO_CODE_CHANGE, and NO_CHANGE.
  • _number: The patch set number.
  • created: The timestamp of when the patch set was created.
  • uploader: The uploader of the patch set as an AccountInfo entity.
  • ref: The Git reference for the patch set.
  • fetch: Information about how to fetch this patch set. The fetch information is provided as a map that maps the protocol name (β€œgit”, β€œhttp”, β€œssh”) to FetchInfo entities. This information is only included if a plugin implementing the download commands interface is installed.
  • commit: The commit of the patch set as CommitInfo entity.
  • files: The files of the patch set as a map that maps the file names to FileInfo entities. Only set if CURRENT_FILES or ALL_FILES option is requested.
  • actions: Actions the caller might be able to perform on this revision. The information is a map of view name to ActionInfo entities.
  • reviewed: Indicates whether the caller is authenticated and has commented on the current revision. Only set if REVIEWED option is requested.
  • messageWithFooter: If the COMMIT_FOOTERS option is requested and this is the current patch set, contains the full commit message with Gerrit-specific commit footers, as if this revision were submitted using the Cherry Pick submit type.
  • push_certificate: If the PUSH_CERTIFICATES option is requested, contains the push certificate provided by the user when uploading this patch set as a PushCertificateInfo entity. This field is always set if the option is requested; if no push certificate was provided, it is set to an empty object.

You are using what serde_json calls the strongly typed Rust data structure, but as the versions are rapidly changing, this breaks your existing code. Correct? It seems to me that you have (at least) two wildly-different options:


  1. If you want to continue using the strongly-typed deserialization, it appears your only choice is to make structs for every version of the server you wish to support, and then detect what version of the server you are dealing with, then use the correct structs in deserialization. You'll have to combine this with traits that each struct implements so you can use them throughout your code.

Pros: You get ALL the data correctly and quickly for the versions you support.

Cons: Your program completely breaks each time a new version changes anything.


  1. You could consider adopting what serde_json calls the untyped or loosely typed representation approach, where you write your own custom logic to extract individual fields that you care about from the json data, and adapt those fields to your one-true-struct representation. That means a lot more deserialization logic for you to write, but after deserialization, everything is simple.

Pros: If you don't care about (or don't use) the fields that change between versions, your program just keeps working when the next version of the server comes out.

Cons: You bear more of the burden of deserialization and might have to deal with a single structure whose member fields might mean different things depending on the version of the server you are dealing with.


I would love to hear how you end up resolving the issue. Especially if you take some other route not yet mentioned.

2 Likes

As an extension to option 1, you could actually define a Gerrit version-trait and use it to pick the right response types:

trait Gerrit {
    type RevisionInfo: RevisionInfo;
    type DiffInfo: DiffInfo;
    // You can put some default functions here if you want them to be overridable on a per-version basis.
}

trait RevisionInfo {
    fn is_draft() -> bool;
    // ...
}

struct GerritV1;

impl Gerrit for GerritV1 { /* ... */ }

As an extension to 2, you can avoid manually parsing by using intermediate structs:

struct RevisionInfo { /* ... */ }

#[derive(Deserialize)]
struct RevisionInfoV1 { /* ... */ }

impl From<RevisionInfoV1> for RevisionInfo { /* ... */ }

#[derive(Deserialize)]
struct RevisionInfoV2 { /* ... */ }

impl From<RevisionInfoV2> for RevisionInfo { /* ... */ }
1 Like

I would use an untagged enum. Enum representations Β· Serde

#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RevisionInfo9 {
    /* fields */
}

#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RevisionInfo13 {
    /* fields */
}

#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum RevisionInfo {
    Gerrit9(RevisionInfo9),
    Gerrit13(RevisionInfo13),
}

This requires Serde 0.9.

2 Likes

This is why I love this forum. I learn so much!

That's beautiful and horrible at the same time (duck typing when you know the correct type is icky).

1 Like

Thanks to all @stebalien, @CleanCut and @dtolnay. I used the way described by @dtolnay. Its very uncomplicated to use serde for such things.
I have done my work in this commit.

2 Likes

For benefit of those, who came across this discussion, author reverted accepted approach within a month , presumably because dealing with enums in your business logic code is too painful, even if it makes serialization neater.

2 Likes