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

Try not flatten payload #2020

Merged
merged 13 commits into from
Sep 27, 2023
Merged

Try not flatten payload #2020

merged 13 commits into from
Sep 27, 2023

Conversation

qiaozha
Copy link
Member

@qiaozha qiaozha commented Sep 15, 2023

fixes #2016

@qiaozha
Copy link
Member Author

qiaozha commented Sep 18, 2023

@joheredi @xirzec I want to confirm about the parameter order in flattened case.
Let's say we have tsp defined as

alias Widget = {
  @visibility("read", "update")
  @path
  id: string;

  weight: int32;

  @query
  color: "red" | "blue";
};

interface Widgets {
  @patch update(@path id1: string, @query color1: string, ...Widget): Widget;
}

what should the update method signature be ?

Option1:
we follow the parameter that is defined in the typespec, regardless of what their location should be. we will get id1, color1, id, weight, color

Option2:
we follow pattern like path > query > body flattened param, and inside path or query or body, we follow the typespec defined order, we will get
id1, id, color1, color, weight

Another question about the path parameters order is what if the path is something like /ids/{id}/id1s/{id1} in the above case ?
should we have id, id1, color1, color, weight if we go to option2 ?

As parameters order doesn't really matter to a API, so the way how OpenAPI3 emitter handles the parameter order may not be very helpful to us. see the typespec playground link here

@xirzec
Copy link
Member

xirzec commented Sep 18, 2023

@qiaozha I think my preference is option 1 where we follow the spec order, since then the spec author can control it by reordering the parameters. We'd still extract any optional values and put them in an options bag at the end, right?

@qiaozha
Copy link
Member Author

qiaozha commented Sep 19, 2023

As much as I prefer the order that is defined in typespec, I have to point out sometimes service team could be too casual about the parameters order from version to version which is not an api level breaking but is a SDK breaking. and we have to do things either asking service team to change the order back or adding workaround manually to avoid unnecessary SDK breaking changes. I am hoping we could do something in modular to reduce the communicating and manual effort.

@xirzec
Copy link
Member

xirzec commented Sep 19, 2023

As much as I prefer the order that is defined in typespec, I have to point out sometimes service team could be too casual about the parameters order from version to version which is not an api level breaking but is a SDK breaking. and we have to do things either asking service team to change the order back or adding workaround manually to avoid unnecessary SDK breaking changes. I am hoping we could do something in modular to reduce the communicating and manual effort.

Yeah, if you think it will reduce churn to follow the same convention as Python I'm okay with it, though they have some behavior we can't directly map like headers always being kwargs (we could pull them into options but that might mean we have required options)

@qiaozha
Copy link
Member Author

qiaozha commented Sep 21, 2023

Confirmed with @joheredi offline, we will keep the original order, as Option2 can only resolve parameter order changes between different parameter location and if there's changes inside each parameter location, it will still be problematic.

@xirzec
Copy link
Member

xirzec commented Sep 21, 2023

It feels a bit odd to have the new body params models be suffixed with Options given many of those operation methods still have another Options type at the end. It's also strange to call it Options when it has a required property, especially when it's the only required property.

It makes it tempting to flatten when there is exactly one required property in the body... in theory the service can never change to add another required body property without a break, yes? If they added an optional body property we could pull that into the actual options bag.

@qiaozha
Copy link
Member Author

qiaozha commented Sep 22, 2023

@xirzec I am not sure I totally get your point. In the below case, prop2, prop3 and prop5 all belong to a body parameter's properties. excapt the body parameter's own name is ""

    alias Foo = {
      @path
      prop1: string;
      prop2: int64;
      prop3?: utcDateTime;
      @query
      prop4: offsetDateTime;
      prop5?: Bar;
    };
    model Bar {
      prop1: string;
      prop2: int64;
    }
    op read(@path pathParam: string, ...Foo, @query queryParam: string): OkResponse;

As we agree to flatten the body payload in this scenario, we will flatten all of them to the top level and because prop2 is required, it will go into the operation signature, prop3 and prop5 are optional, they will go into the option bag the same as the other optional ordinary query/header parameters if there's any.
So our operation signature would loook like

      export interface ReadOptions extends OperationOptions {
        prop3?: Date;
        prop5?: Bar;
      }
      export async function read(
        context: Client,
        pathParam: string,
        prop1: string,
        prop4: string,
        queryParam: string,
        prop2: number,
        options: ReadOptions = { requestOptions: {} }
      ):

And we build our payload like

        return context
          .path("/{pathParam}/{prop1}", pathParam, prop1)
          .post({
            ...operationOptionsToRequestParameters(options),
            queryParameters: { prop4: prop4, queryParam: queryParam },
            body: {
              prop2: prop2,
              prop3: options?.prop3?.toISOString(),
              prop5: {
                prop1: options?.prop5?.["prop1"],
                prop2: options?.prop5?.["prop2"],
              },
            },
          });

If there're more required body properties in the above case, they will just go to the operation signature the same as other prop2. not in the option bag.

Let me know if it makes sense to you. Thanks

@qiaozha
Copy link
Member Author

qiaozha commented Sep 22, 2023

.. in theory the service can never change to add another required body property without a break, yes?

To answer this question, I think it's a breaking no matter we flatten it or not. if we don't flatten, the breaking will be a model layer breaking, if we flatten it, it will be an operation signature breaking.

@xirzec
Copy link
Member

xirzec commented Sep 22, 2023

Sorry, I think it will help if I give specific examples that I was looking at in the generated output:

export interface AddOrUpdateBlockItemsOptions {
  blockItems: TextBlockItemInfo[];
}


export class ContentSafetyClient {
  addOrUpdateBlockItems(blocklistName: string, body: AddOrUpdateBlockItemsOptions, options?: AddOrUpdateBlockItemsRequestOptions): Promise<AddOrUpdateBlockItemsResult>;
}

In the above it would feel better to name the first interface something like AddOrUpdateBlockItemsBody. At that point it makes better sense to me and works, especially in cases where we have many required body properties.

So now it would look like:

export interface AddOrUpdateBlockItemsBody {
  blockItems: TextBlockItemInfo[];
}


export class ContentSafetyClient {
  addOrUpdateBlockItems(blocklistName: string, body: AddOrUpdateBlockItemsBody, options?: AddOrUpdateBlockItemsRequestOptions): Promise<AddOrUpdateBlockItemsResult>;
}

In this specific case though, I was thinking it is a little silly to have an entire object just for one property, so as an optimization we could flatten bodies that have a single required property so that the above looked like:

export class ContentSafetyClient {
  addOrUpdateBlockItems(blocklistName: string, blockItems: TextBlockItemInfo[], options?: AddOrUpdateBlockItemsRequestOptions): Promise<AddOrUpdateBlockItemsResult>;
}

Does that make sense? So less of a global rule and more of a specific optimization for body objects with only one required property.

@qiaozha
Copy link
Member Author

qiaozha commented Sep 23, 2023

AddOrUpdateBlockItemsOptions is the name from their typespec model, it's not generated by codegen. I specifically checked with them, they wouldn't like to change. see comment here Azure/azure-sdk-for-js#26542 (comment)
In their latest typespec, they have changed it into AddBlockItemsOptions and the options still there https://github.com/Azure/azure-rest-api-specs/blob/main/specification/cognitiveservices/ContentSafety/models.tsp#L180

I have to say, I have mixed feelings about flatten one required property body payload.

  1. it indeed makes the user experience better, and it's one of the main reason we have x-ms-client-flatten in swagger.
  2. from breaking change perspective.
    a. as state here Try not flatten payload #2020 (comment) adding a required property is a breaking either way
    b. if it's making this required property optional, but keep the body parameter required, in the case of not flatten, it will not be a breaking but in the case of flatten, it will be a breaking.
    c. In the case of body parameter is optional, current HLC implementation is to put the body parameter into the option bag, Modular is still keep the body in the operation signature. so changing both that required property and the body parameter itself from required to non-required
    c1: in the case of not flatten, it will be a breaking for hlc and will be a non breaking for Modular.
    c2: in the case of flatten, it will be a breaking for both hlc and Modular.
  3. flatten will overall make the serialize and deserialize more complex. And as we are actually flatten the body payload not the model, it will cause inconsistency between the request and response if that body model is being used in a roundtrip case. One of the reason I don't want to flatten body payload is that I want to have a model based serialize and deserialize in Modular as suggested here Should not flatten first layer of payload in Modular. #2016 (comment).
  4. As x-ms-client-flatten has been used in so many places in current rest api specs repo https://github.com/search?q=repo%3AAzure%2Fazure-rest-api-specs%20x-ms-client-flatten&type=code, it will be unavoidable to support model flatten unless we want a very large scale of breaking changes when they migrate to typespec.

My preference would be we could support one required property flatten in model based flatten when we get to the x-ms-client-flatten thing ?

@MaryGao
Copy link
Contributor

MaryGao commented Sep 25, 2023

Does that make sense? So less of a global rule and more of a specific optimization for body objects with only one required property.

I agree with Qiaoqiao that not setting this rule for one required property. This may introduce un-necessary breakings and also introduce complexity for specific conditions.

For the overall pr it looks good to me and as we discussed we still have the requirement of flattening especially for mgmt SDKs and I prefer we continue monitoring this topic.

Copy link
Contributor

@MaryGao MaryGao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left small comments, overall LGTM.

@qiaozha
Copy link
Member Author

qiaozha commented Sep 27, 2023

@joheredi I will merge this PR first, let me know if you have other concerns.

Also, please note, we will need to work with OpenAI to use spread of alias if they want a flattened operation sigature.

@qiaozha qiaozha merged commit 5e92ed8 into Azure:main Sep 27, 2023
28 checks passed
jeremymeng pushed a commit to jeremymeng/autorest.typescript that referenced this pull request Sep 27, 2023
* try-not-flatten-payload

* fix tests

* should not generate duplicate models and should not generate body schema properties in options

* fix model property as path parameter

* format

* fix ci

* add ut for alias flattening

* add ut for not flatten if spread models

* add deserialize for flattened body

* enable cadl ranch test for spread in modular
@qiaozha qiaozha mentioned this pull request Jul 11, 2024
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

Successfully merging this pull request may close these issues.

Should not flatten first layer of payload in Modular.
5 participants