Azure / autorest.csharp

Extension for AutoRest (https://github.com/Azure/autorest) that generates C# code
MIT License
142 stars 166 forks source link

Support multiple content types as input #3056

Open pshao25 opened 1 year ago

pshao25 commented 1 year ago

Sample swagger: We want 4 content types: "text/plain", "application/json", "image/jpeg", "image/png"

"post": {
  "operationId": "params_postParameters",
  "description": "POST a JSON or a JPEG",
  "consumes": ["text/plain", "application/json", "image/jpeg", "image/png"],
  "parameters": [
    {
      "name": "parameter",
      "in": "body",
      "description": "I am a body parameter with a new content type.",
      "schema": {
        "$ref": "#/definitions/PostInput"
      },
      "required": true
    }
  ],
  "responses": {
    "200": {
      "description": "Answer from service",
      "schema": {
        "type": "object"
      }
    }
  }
}

Cadl: Example in cadl-ranch: https://github.com/Azure/cadl-ranch/pull/201

// main.cadl
@route("/serviceDriven/parameters")
@doc("POST a JSON or a JPEG")
@convenienceAPI
op postParameters(parameter: PostInput | bytes, @header contentType: "text/plain" | "application/json" | "image/jpeg" | "image/png"): object;

We expect user writes this. We could have internal mapping like M4 (how it maps needs to confirm with Tim):

Generated SDK:

// protocol method: ContentType is `Azure.Core.ContentType` containing all types of content type
public virtual Response PostParameters(RequestContent content, ContentType contentType, RequestContext context = null);

// convenience method for model
public virtual Response<BinaryData> PostParameters(PostInput parameter, ContentType contentType, CancellationToken cancellationToken = default);
// convenience method for bytes
public virtual Response<BinaryData> PostParameters(BinaryData parameter, ContentType contentType, CancellationToken cancellationToken = default);
// convenience method for string
public virtual Response<BinaryData> PostParameters(string parameter, ContentType contentType, CancellationToken cancellationToken = default);

Above examples are under the condition if there are more than 1 content types for the specific type. If there is only one content type, we omit ContentType parameter. For example, this case we will generate:

// protocol method: ContentType is `Azure.Core.ContentType` containing all types of content type
public virtual Response PostParameters(RequestContent content, ContentType contentType, RequestContext context = null);

// convenience method for "application/json"
public virtual Response<BinaryData> PostParameters(PostInput parameter, CancellationToken cancellationToken = default);
// convenience method for "image/jpeg", "image/png"
public virtual Response<BinaryData> PostParameters(BinaryData parameter, ContentType contentType, CancellationToken cancellationToken = default);
// convenience method for "text/plain"
public virtual Response<BinaryData> PostParameters(string parameter, CancellationToken cancellationToken = default);

Fine tune mapping:

// client.cadl
@overload(postParameters)
@convenienceAPI
// Here user maps "text/plain" to model type
op postParametersApplicationJson(parameter: PostInput, @header contentType: "text/plain" | "application/json" ): object;
@overload(postParameters)
@convenienceAPI
op postParametersImage(parameter: bytes, @header contentType: "image/jpeg" | "image/png"): object;

The generated SDK becomes:

// protocol method: ContentType is `Azure.Core.ContentType` containing all types of content type
public virtual Response PostParameters(RequestContent content, ContentType contentType, RequestContext context = null);

// convenience method for "text/plain", "application/json"
public virtual Response<BinaryData> PostParameters(PostInput parameter, ContentType contentType, CancellationToken cancellationToken = default);
// convenience method for "image/jpeg", "image/png"
public virtual Response<BinaryData> PostParameters(BinaryData parameter, ContentType contentType, CancellationToken cancellationToken = default);

Remaining issues:

pshao25 commented 1 year ago

Decision: ContentType of concenience method should be Azure.Core.ContentType

This how we map a media type string to category in M4:

export function knownMediaType(mediaType: string) {
  const mt = parseMediaType(mediaType);
  if (mt) {
    if ((mt.subtype === json || mt.suffix === json) && (mt.type === application || mt.type === text)) {
      return KnownMediaType.Json;
    }
    if ((mt.subtype === xml || mt.suffix === xml) && (mt.type === application || mt.type === text)) {
      return KnownMediaType.Xml;
    }
    if (mt.type === "audio" || mt.type === "image" || mt.type === "video" || mt.subtype === "octet-stream") {
      return KnownMediaType.Binary;
    }
    if (mt.type === application && mt.subtype === formEncoded) {
      return KnownMediaType.Form;
    }
    if (mt.type === "multipart" && mt.subtype === "form-data") {
      return KnownMediaType.Multipart;
    }
    if (mt.type === application) {
      // at this point, an unrecognized application/* is considered a binary format
      // since we don't have any other way of dealing with it.
      return KnownMediaType.Binary;
    }
    if (mt.type === "text") {
      return KnownMediaType.Text;
    }
  }
  return KnownMediaType.Unknown;
}

export function parseMediaType(mediaType: string) {
  if (mediaType) {
    const parsed =
      /(application|audio|font|example|image|message|model|multipart|text|video|x-(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+))\/([0-9A-Za-z!#$%&'*.^_`|~-]+)\s*(?:\+([0-9A-Za-z!#$%&'*.^_`|~-]+))?\s*(?:;.\s*(\S*))?/g.exec(
        mediaType,
      );
    if (parsed) {
      return {
        type: parsed[1],
        subtype: parsed[2],
        suffix: parsed[3],
        parameter: parsed[4],
      };
    }
  }
  return undefined;
}
pshao25 commented 1 year ago

All the content type string with this pattern is valid:

/(application|audio|font|example|image|message|model|multipart|text|video|x-(?:[0-9A-Za-z!#$%&'*+.^_`|~-]+))\/([0-9A-Za-z!#$%&'*.^_`|~-]+)\s*(?:\+([0-9A-Za-z!#$%&'*.^_`|~-]+))?\s*(?:;.\s*(\S*))?/

And they will be categoried into one of Binary, Form, Json, Multipart, Text, Xmlby above logic.

KnownMediaType.Json:

// Only one media type
public virtual Response PostParameters(Model parameter, CancellationToken cancellationToken = default)
{
    Response response = PostParameters(parameter.ToRequestContent(), "application/json", context);
}

// More than one media type
public virtual Response PostParameters(Model parameter, ContentType contentType, CancellationToken cancellationToken = default)
{
    Response response = PostParameters(parameter.ToRequestContent(), contentType, context);
}

KnownMediaType.Binary

// Only one media type
public virtual Response PostParameters(BinaryData parameter, CancellationToken cancellationToken = default)
{
    Response response = PostParameters(parameter, "image/jpeg", context);
}

// More than one media type
public virtual Response PostParameters(BinaryData parameter, ContentType contentType, CancellationToken cancellationToken = default)
{
    Response response = PostParameters(parameter, contentType, context);
}

KnownMediaType.Text

// Only one media type
public virtual Response PostParameters(string parameter, CancellationToken cancellationToken = default)
{
    Response response = PostParameters(parameter, "text/plain", context);
}

// More than one media type
public virtual Response PostParameters(string parameter, ContentType contentType, CancellationToken cancellationToken = default)
{
    Response response = PostParameters(parameter, contentType, context);
}

KnownMediaType.Xml (e.g. application/xml) We just don't support. KnownMediaType.Form (e.g. application/x-www-form-urlencoded) We just don't support. KnownMediaType.Multipart We just don't support.

No matter how many media types, only one protocol method and one request method

public virtual Response PostParameters(RequestContent content, ContentType contentType, RequestContext context = null)
{
    using HttpMessage message = CreatePostParametersRequest(content, contentType, context);
}

internal HttpMessage CreatePostParametersRequest(RequestContent content, ContentType contentType, RequestContext context)
{
    request.Headers.Add("content-type", contentType.ToString());
    request.Content = content;
}

Invalid cases handling:

  1. When the content types listed in the header don't have corresponding body types. For example, "application/json" should map to a model, but the paramter type is bytes | string.

    op postParameters(@body parameter: bytes | string, @header contentType: "application/json" | "image/jpeg"): void;

    Same cases are "image/jpeg" is in contentType but bytes is not in the type of parameter.

  2. When the content types contain "application/json", but more than one models are listed in parameter. We don't which model to use.

    op postParameters(@body parameter: Model1 | Model2, @header contentType: "application/json"): void;
  3. In the overload method, more than one types of parameter are listed. In the below example, the type of parameter is Model | string. More than one types. Therefore, we cannot describe it in one method.

    @overload(postParameters)
    op postParametersApplicationJson(parameter: Model | string, @header contentType: "text/plain" | "application/json" ): void;
  4. When both json and xml exist, their signatures are the same, then how could we decide which content writer to use?

Corner cases handling:

  1. Overload methods not cover base operation:
    
    @overload(postParameters)
    op postParametersImage(@body parameter: bytes, @header contentType: "image/jpeg" | "image/png"): void;

@route("/serviceDriven/parameters") op postParameters(@body parameter: bytes | Thing, @header contentType: "application/json" | "image/jpeg" | "image/png"): void;

There is only one `postParametersImage`. No `postParametersJson`.

2. Overload method has `convenientAPI` while base operation not, or the other way around. Same situation to `protocolAPI`.

@overload(postParameters) @convenientAPI(true) op postParametersImage(@body parameter: bytes, @header contentType: "image/jpeg" | "image/png"): void;

@route("/serviceDriven/parameters") @convenientAPI(false) op postParameters(@body parameter: bytes | Thing, @header contentType: "application/json" | "image/jpeg" | "image/png"): void;

pshao25 commented 1 year ago

The decision of Java is:

  1. Union body type and union header type are not allowed together We should use @overload to define multiple content scenario. There will be linter to prevent the definition which defines both body type as union and header type as union. User should define overload if they define body type as union and header type as union.
  2. Single body type and union header type are allowed An operation marked as the @overload will generate corresponding convenience method with the mapping it defines, no matter whether there is @convenientAPI or whether the mapping is reasonable.
  3. 'shared route' feature would be taken as normal operation.
pshao25 commented 1 year ago

Decision on Feb 23rd:

  1. we first support multiple content types with only one body type.
  2. how to represent cadl still needs to be discussed.
pshao25 commented 1 year ago

We don't take multiple content type as a special case. Any cases with overload or shared route come up, we will just discuss overload or shared route.