tsync icon indicating copy to clipboard operation
tsync copied to clipboard

Failing for Internally/Adjacently tagged

Open joaoantoniocardoso opened this issue 9 months ago • 23 comments

I need to interact with an external API that uses Adjacentelly tagged enums, like the following:

#[derive(Serialize, Deserialize)]
#[tsync]
pub struct CameraControl {
    pub camera_uuid: String,
    #[serde(flatten)]
    pub action: Action,
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "action", content = "json")]
//#[serde(tag = "action")] // This also fails, same output
#[tsync]
pub enum Action {
    GetVideoParameterSettings(VideoParameterSettings),
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
#[tsync]
pub struct VideoParameterSettings {
    pub frame_rate: Option<u16>,
}

This is producing the following:

/* This file is generated and managed by tsync */

type CameraControl = Action & {
  camera_uuid: string;
}

type Action =;

interface VideoParameterSettings {
    frame_rate?: number;
}

If I define it as Externally Tagged (which is not what I need):

#[derive(Serialize, Deserialize)]
#[tsync]
pub struct CameraControl {
    pub camera_uuid: String,
    #[serde(flatten)]
    pub action: Action,
}

#[derive(Serialize, Deserialize)]
//#[serde(tag = "action", content = "json")]
//#[serde(tag = "action")] // This also fails, same output
#[tsync]
pub enum Action {
    GetVideoParameterSettings(VideoParameterSettings),
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
#[tsync]
pub struct VideoParameterSettings {
    pub frame_rate: Option<u16>,
}

The result is:

/* This file is generated and managed by tsync */

type CameraControl = Action & {
  camera_uuid: string;
}

type Action =
  | { "GetVideoParameterSettings": VideoParameterSettings };

interface VideoParameterSettings {
    frame_rate?: number;
}

using tsync = "2.2.1"

Am I missing something?

Thanks!

joaoantoniocardoso avatar Feb 25 '25 22:02 joaoantoniocardoso

I have a bit of time rn so I might be able to take a crack at this, could you tell me more about the expected output in this case?

AnthonyMichaelTDM avatar Feb 25 '25 23:02 AnthonyMichaelTDM

okay this seems to be a bug with how tuple variants are handled, probably related to #43 and #46

AnthonyMichaelTDM avatar Feb 25 '25 23:02 AnthonyMichaelTDM

I have a bit of time rn so I might be able to take a crack at this, could you tell me more about the expected output in this case?

Nice! Thanks for the fast answer. I'm open to any output I can work with on the typescript side, as I'm still implementing the code.

One possibility would be something like:


interface GetVencConfAction {
  action: "getVencConf";
  json: VideoParameterSettings;
}

type CameraControl = {
  camera_uuid: string;
} & Action;

type Action =
  | GetVencConfAction;

interface VideoParameterSettings {
    frame_rate?: number;
}

joaoantoniocardoso avatar Feb 25 '25 23:02 joaoantoniocardoso

what about something like this?

/* This file is generated and managed by tsync */

type CameraControl = Action & {
  camera_uuid: string;
}

type Action =
  | Action__GetVideoParameterSettings;

type Action__GetVideoParameterSettings = {
  "action": "GetVideoParameterSettings";
  "json": VideoParameterSettings;
};

interface VideoParameterSettings {
  frame_rate?: number;
}

AnthonyMichaelTDM avatar Feb 26 '25 01:02 AnthonyMichaelTDM

I don't really use typescript, so I'm not 100% sure this is valid syntax

AnthonyMichaelTDM avatar Feb 26 '25 01:02 AnthonyMichaelTDM

working on a PR that could resolve #58, #43, and #46.

so far, this input:

#[tsync]
#[derive(Serialize, Deserialize)]
pub struct CameraControl {
    pub camera_uuid: String,
    #[serde(flatten)]
    pub action: Action,
}

#[tsync]
#[derive(Serialize, Deserialize)]
#[serde(tag = "action", content = "json")]
// #[serde(tag = "action")] // This also fails, same output
pub enum Action {
    GetVideoParameterSettings(VideoParameterSettings),
    SomeTupleVariant(String, u32),
    SomeOtherAction {
        some_parameters: SomeOtherActionParameters,
    },
    SomethingElseWithoutParameters,
}

#[tsync]
#[derive(Serialize, Deserialize)]
pub struct SomeTupleStruct(String, u32);

#[tsync]
#[derive(Serialize, Deserialize)]
pub struct VideoParameterSettings {
    pub frame_rate: Option<u16>,
}

#[tsync]
#[derive(Serialize, Deserialize)]
pub struct SomeOtherActionParameters {
    pub some_other_field: String,
}

produces this output:

/* This file is generated and managed by tsync */

type CameraControl = Action & {
  camera_uuid: string;
}

type Action =
  | Action__GetVideoParameterSettings
  | Action__SomeTupleVariant
  | Action__SomeOtherAction
  | Action__SomethingElseWithoutParameters;

type Action__GetVideoParameterSettings = {
  "action": "GetVideoParameterSettings";
  "json": VideoParameterSettings;
};
type Action__SomeTupleVariant = {
  "action": "SomeTupleVariant";
  "json": [ string, number ];
};
type Action__SomeOtherAction = {
  action: "SomeOtherAction";
  some_parameters: SomeOtherActionParameters;
};
type Action__SomethingElseWithoutParameters = {
  action: "SomethingElseWithoutParameters";
};

type SomeTupleStruct = [ string, number ]

interface VideoParameterSettings {
  frame_rate?: number;
}

interface SomeOtherActionParameters {
  some_other_field: string;
}

AnthonyMichaelTDM avatar Feb 26 '25 01:02 AnthonyMichaelTDM

should type SomeTupleStruct = [ string, number ] end in a semi-colon?

AnthonyMichaelTDM avatar Feb 26 '25 02:02 AnthonyMichaelTDM

That output looks great =)

should type SomeTupleStruct = [ string, number ] end in a semi-colon?

it doesn't hurt to add, but if you prefer not to, the interpreter/engine will add it (ASI)

joaoantoniocardoso avatar Feb 26 '25 03:02 joaoantoniocardoso

btw, I'll test your PR on my full code tomorrow, ty!

joaoantoniocardoso avatar Feb 26 '25 04:02 joaoantoniocardoso

lmk how that goes

AnthonyMichaelTDM avatar Feb 26 '25 04:02 AnthonyMichaelTDM

lmk how that goes

I've just tested it using your PR branch and the generated code is perfect for my use :)

joaoantoniocardoso avatar Feb 26 '25 12:02 joaoantoniocardoso

For my final application, the only missing thing is the serde field rename (#42). Let me know if I can help!

joaoantoniocardoso avatar Feb 26 '25 12:02 joaoantoniocardoso

I'll try to take a look at this on the weekend Anthony. Thanks for improving the community's rust tools! :)

Wulf avatar Mar 04 '25 04:03 Wulf

Hi @AnthonyMichaelTDM, I was testing further your PR and noticed it is failing for the following case:


#[derive(Deserialize, Serialize)]
#[tsync]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum CaptureConfiguration {
    Video(VideoCaptureConfiguration),
    Redirect(RedirectCaptureConfiguration),
}

#[derive(Deserialize, Serialize)]
#[tsync]
pub struct VideoCaptureConfiguration {
    pub height: u32,
    pub width: u32,
}

#[derive(Deserialize, Serialize)]
#[tsync]
pub struct RedirectCaptureConfiguration {}

The expected would be:

type CaptureConfiguration =
  | ({ type: "video" } & VideoCaptureConfiguration)
  | ({ type: "redirect" } & RedirectCaptureConfiguration)

interface VideoCaptureConfiguration {
  height: number;
  width: number;
}

interface RedirectCaptureConfiguration {
  [key: PropertyKey]: never // note: this is an empty object
}

But the current (from your PR) output is:

type CaptureConfiguration =;
export interface VideoCaptureConfiguration {
  height: number;
  width: number;
}

interface RedirectCaptureConfiguration = {
};

Two things: it's (1) failing to generate the internally tagged "type" for "CaptureConfiguration", and it's (2) failing to generate an empty object type. Note that: {} is not an empty object in typescript. I can easily overcome this empty-object thing using a regex after generation, but I think the (1) is very important to be fixed.

What can we do about it?

Thanks

joaoantoniocardoso avatar Mar 10 '25 15:03 joaoantoniocardoso

yeah, so without specifying the content attribute, it'll try to do internal tagging, but fail (if you run it with the -d argument you'll see the errors)

to fix this, either don't specify a tag (external tagging):

#[derive(Deserialize, Serialize)]
#[tsync]
#[serde(rename_all = "lowercase")]
pub enum CaptureConfiguration {
    Video(VideoCaptureConfiguration),
    Redirect(RedirectCaptureConfiguration),
}

#[derive(Deserialize, Serialize)]
#[tsync]
pub struct VideoCaptureConfiguration {
    pub height: u32,
    pub width: u32,
}

#[derive(Deserialize, Serialize)]
#[tsync]
pub struct RedirectCaptureConfiguration {}
to get
/* This file is generated and managed by tsync */

type CaptureConfiguration =
  | { "video": VideoCaptureConfiguration }
  | { "redirect": RedirectCaptureConfiguration };

interface VideoCaptureConfiguration {
  height: number;
  width: number;
}

interface RedirectCaptureConfiguration {
}

or set the content attribute (adjacent tagging):

#[derive(Deserialize, Serialize)]
#[tsync]
#[serde(tag = "type", content = "data", rename_all = "lowercase")]
pub enum CaptureConfiguration {
    Video(VideoCaptureConfiguration),
    Redirect(RedirectCaptureConfiguration),
}

#[derive(Deserialize, Serialize)]
#[tsync]
pub struct VideoCaptureConfiguration {
    pub height: u32,
    pub width: u32,
}

#[derive(Deserialize, Serialize)]
#[tsync]
pub struct RedirectCaptureConfiguration {}
to get
/* This file is generated and managed by tsync */

type CaptureConfiguration =
  | CaptureConfiguration__Video
  | CaptureConfiguration__Redirect;

type CaptureConfiguration__Video = {
  "type": "Video";
  "data": VideoCaptureConfiguration;
};
type CaptureConfiguration__Redirect = {
  "type": "Redirect";
  "data": RedirectCaptureConfiguration;
};

interface VideoCaptureConfiguration {
  height: number;
  width: number;
}

AnthonyMichaelTDM avatar Mar 11 '25 03:03 AnthonyMichaelTDM

I see, thanks for the ideas, but for this CaptureConfiguration case, I need it internally tagged, like:

type CaptureConfiguration =
  | ({ type: "video" } & VideoCaptureConfiguration)
  | ({ type: "redirect" } & RedirectCaptureConfiguration);

For now, I'm using ts-rs for that, and using this lib when I need numbered enums, which is something this crate does very well! :)

thanks!

joaoantoniocardoso avatar Mar 11 '25 05:03 joaoantoniocardoso

https://serde.rs/enum-representations.html

AnthonyMichaelTDM avatar Mar 11 '25 16:03 AnthonyMichaelTDM

https://serde.rs/enum-representations.html

It would be perfect to me if this lib could support all those enum representations, because I do need to use all of them: we don't always have control over the APIs we are working with. In my specific case, some use externally tagged, some internally tagged, and others adjacently tagged. On top of that, they use different casing styles (camelCase, PascalCase, snake_case, etc.), so I need to just be flexible.

Given the example from the serde docs, I would expect the following Rust enum:

#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Message {
    Request { id: String, method: String, params: Params },
    Response { id: String, result: Value },
}

can represent the following two json objects:

{
  "type": "Request",
  "id": "123",
  "method": "getData",
  "params": {...}
}

{
  "type": "Response",
  "id": "123",
  "result": {...}
}

which can be represented in typescript as:

interface Request {
    id: string;
    method: string;
    params: Params;
}

interface Response {
    id: string;
    result: Value;
}

type Message =
  | ({ type: "Request" } & Request)
  | ({ type: "Response" } & Response);

Does this sound ok?

Also, just to clarify -- there's no rush or pressure. I truly appreciate the work you've already done; it has literally saved me this month with the numbered enum representation!

Thanks for taking the time and effort!

joaoantoniocardoso avatar Mar 11 '25 17:03 joaoantoniocardoso

That makes sense, I'm just not sure that it's entirely correct if that makes sense?

Just sending the link wasn't very .. helpful of me, sorry about that. What I meant was that, according to that link, giving a tag for an enum that has tuple variants is a compile error.

That leaves me with two questions:

  1. what is the correct type for an empty object?
  2. is what you want functionally different from using internal tagging but with struct variants, like:
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Message {
    Request { id: String, method: String, params: Params },
    Response { id: String, result: Value },
}

or for your case, I suppose:

#[derive(Deserialize, Serialize)]
#[tsync]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum CaptureConfiguration {
    Video {
        height: u32,
        width: u32,
    },
    Redirect,
}

which generates:

type CaptureConfiguration =
  | CaptureConfiguration__Video
  | CaptureConfiguration__Redirect;

type CaptureConfiguration__Video = {
  type: "video";
  height: number;
  width: number;
};
type CaptureConfiguration__Redirect = {
  type: "redirect";
};

AnthonyMichaelTDM avatar Mar 12 '25 18:03 AnthonyMichaelTDM

Oh right, newtype variants!

It says they should be supported, and your example qualifies as one.

I'll take a crack at it but can't make any promises

AnthonyMichaelTDM avatar Mar 12 '25 18:03 AnthonyMichaelTDM

what is the correct type for an empty object?

interface EmptyObjectExample {
  [key: PropertyKey]: never
}

is what you want functionally different from using internal tagging but with struct variants, like: Oh right, newtype variants!

Yes, that's almost the same, except I need the internal structs (the newtype variants) to be exported as well :)

I'll take a crack at it but can't make any promises

awesome, let me know if you need anything from my side!

joaoantoniocardoso avatar Mar 12 '25 20:03 joaoantoniocardoso

I'm actually almost done, it was easier than I thought. Just doing final cleanup before I commit the changes

AnthonyMichaelTDM avatar Mar 12 '25 21:03 AnthonyMichaelTDM

Alright, pushed. Go ahead and take a look, I'll fix empty object typing next

AnthonyMichaelTDM avatar Mar 12 '25 22:03 AnthonyMichaelTDM