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

uri template in RLC #2814

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft

uri template in RLC #2814

wants to merge 19 commits into from

Conversation

MaryGao
Copy link
Member

@MaryGao MaryGao commented Sep 11, 2024

fixes #2793
fixes #2792

Possible values

  • allowReserved: true/false
  • value: primitive/array/object
  • explode: true/false
  • style: form/spaceDelimited/pipeDelimited

In-scoped cases

Path allowReserved

Support to define allowReserved for path parameter but the style and explode support for path parameter is not in-scope of this pr. The default for allowReserved would be

  • allowReserved = false
allowReserved Supported
true y
false y

Query expansion

Please note the allowReserved for query paremeter is not in scope of this pr. Except the form style we would also include the spaceDelimited and pipeDelimited mainly considering backward-compatibility. The default setting would be:

  • style = form & explode = false
Style Explode Uri Template Primitive value id = 5 Array id = [3, 4, 5] Object id = {"role": "admin", "firstName": "Alex"} Supported
form false /users{?id} /users?id=5 /users?id=3,4,5 /users?id=role,admin,firstName,Alex y
form* true* /users{?id*} /users?id=5 /users?id=3&id=4&id=5 /users?role=admin&firstName=Alex y
spaceDelimited false n/a n/a /users?id=3%204%205 n/a y
pipeDelimited false n/a n/a /users?id=3|4|5 n/a y

We also have support for case in OpenAPI2 which were removed in openapi3 and we will remove this support also and we could dicuss on how to support when we have a real case yet.

  • tsv tab separated format

Header expansion

Any expansion for header parameters would not be covered in this pr. We haven't met any real cases yet and we could expand this idea to header also if any in future.

Detailed cases

Case 1: allowReserved: true + path

    @route("template/{+param}")
    op template(param: string): void;
function withReservedCharacters(value: string): StringWithEncodingMetadata {
    return {
        allowReserved: true,
        value
    }
}
interface StringWithEncodingMetadata {
    value: string;
    allowReserved: true;
}

export declare interface Routes {
  (path: "/routes/path/reserved-expansion/template/{param}", param: StringWithEncodingMetadata):PathParametersReservedExpansionTemplate;
}

Case 2: explode:false array + query

      @route("array{?param}")
      op array(param: string[]): void;
export declare interface QueryParametersQueryExpansionStandardArrayQueryParamProperties {
    param: string[];
}

export declare interface Routes { 
(path: "/routes/query/query-expansion/standard/array"): QueryParametersQueryExpansionStandardArray;
}

Case 3: explode:false object + query

      @route("record{?param}")
      op record(param: Record<int32>): void;
export declare interface QueryParametersQueryExpansionStandardRecordQueryParamProperties {
    param: Record<string, number>;
}

export declare interface Routes { 
(path: "/routes/query/query-expansion/standard/record"): QueryParametersQueryExpansionStandardRecord;
}

Case 4: explode:true array + query

      @route("array{?param*}")
      op array(param: string[]): void;
function withExplodedAndFormStyle(value: string[] | Record<string, unknown>){
    return {
        explode: true,
        style: "form",
        value: value as any
    } as const
}

interface QueryParametersQueryExpansionExplodeArrayQueryParam {
    value: string[];
    explode: true;
    style: "form";
}

export declare interface QueryParametersQueryExpansionExplodeArrayQueryParamProperties {
    param: QueryParametersQueryExpansionExplodeArrayQueryParam;
}

export declare interface Routes { 
(path: "/routes/query/query-expansion/explode/array"): QueryParametersQueryExpansionExplodeArray;
}

Case 5: explode:true object + query

      @route("record{?param*}")
      op record(param: Record<int32>): void;
function withExplodedAndFormStyle(value: string[] | Record<string, unknown>){
    return {
        explode: true,
        style: "form",
        value: value as any
    } as const
}

interface QueryParametersQueryExpansionExplodeRecordQueryParam{
    value: Record<string, number>;
    explode: true;
    style: "form";
}

export declare interface QueryParametersQueryExpansionExplodeRecordQueryParamProperties {
    param: QueryParametersQueryExpansionExplodeRecordQueryParam;
}

export declare interface Routes { 
(path: "/routes/query/query-expansion/explode/record"): QueryParametersQueryExpansionExplodeRecord;
}

Case 6: explode:true primitive + query

      @route("primitive{?param*}")
      op primitive(param: string): void;
export declare interface QueryParametersQueryExpansionExplodePrimitiveQueryParamProperties {
    param: string;
}

export declare interface Routes { 
(path: "/routes/query/query-expansion/explode/primitive"): QueryParametersQueryExpansionExplodePrimitive;
}

Case 7: explode:false array + query + ssv

@route("/ssv")
op ssv(
  @query({
    format: "ssv",
  })
  colors: string[],
): NoContentResponse;
function withNonExplodedAndSpaceStyle(value: string[] | Record<string, unknown>){
    return {
        explode: false,
        style: "spaceDelimited",
        value: value as any
    } as const
}

interface QuerySsvQueryColorType{
    value: string[];
    explode: false;
    style: "spaceDelimited";
}

export declare interface QuerySsvQueryParamProperties {
    colors: QuerySsvQueryColorType;
}

export declare interface Routes { 
(path: "/parameters/collection-format/query/ssv"): QuerySsv;
}

Case 8: explode:false array + query + pipe

@route("/ssv")
op ssv(
  @query({
    format: "ssv",
  })
  colors: string[],
): NoContentResponse;
function withNonExplodedAndPipeStyle(value: string[] | Record<string, unknown>){
    return {
        explode: false,
        style: "pipeDelimited",
        value: value as any
    } as const
}

interface QueryPipesQueryColorsType{
    value: string[];
    explode: false;
    style: "pipeDelimited";
}

export declare interface QuerySsvQueryParamProperties {
    colors: QueryPipesQueryColorsType;
}

export declare interface Routes { 
(path: "/parameters/collection-format/query/pipes"): QueryPipes;
}

Case 9: explode:false array + query + tsv[not supported -> fallback to string]

@route("/tsv")
op tsv(
  @query({
    format: "tsv",
  })
  colors: string[],
): NoContentResponse;
export declare interface QueryTsvQueryParamProperties {
    colors: string;
}

export declare interface Routes { 
(path: "/parameters/collection-format/query/tsv"): QueryTsv;
}

Case 10: explode:false array + query + tsv[not supported -> fallback to string]

@route("/csv")
op csv(
  @doc("Possible values for colors are [blue,red,green]")
  @header({
    format: "csv",
  })
  colors: string[],
): NoContentResponse;
export declare interface HeaderCsvHeaderParam {
    headers: RawHttpHeadersInput & HeaderCsvHeaders;
}

export declare interface HeaderCsvHeaders {
    colors: string;
}

export declare interface Routes { 
(path: "/parameters/collection-format/header/csv"): HeaderCsv;
}

References

.get();
assert.strictEqual(result.status, "204");
});

it("should have explode: true array", async () => {
Copy link
Member Author

@MaryGao MaryGao Sep 25, 2024

Choose a reason for hiding this comment

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

This case failed with 400 issue and I think we build correct url but failed in server side: Azure/cadl-ranch#731.

image

assert.strictEqual(result.status, "204");
});

it("should have explode: true primitive", async () => {
Copy link
Member Author

@MaryGao MaryGao Sep 25, 2024

Choose a reason for hiding this comment

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

This case failed with same reason, I plan to not generate wrapper type for primitive because the explode or style information will have no effect on the final url. Then I think this will not be an issue.

@timovv I think you could decide in core side if we need to support primitive value type.

image

Copy link
Member

Choose a reason for hiding this comment

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

We can not throw here (in that case we would just ignore explode if we don't get an array), but I agree with your plan and think it would be better if we just didn't generate the wrapper in this case, since the combination of primitive and explode: true doesn't really make sense.

assert.strictEqual(result.status, "204");
});

it("should have explode: false record", async () => {
Copy link
Member Author

@MaryGao MaryGao Sep 25, 2024

Choose a reason for hiding this comment

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

This case failed with 400 either. This is a case for non-exploded object. Similar to array I think we should support the same default behavior for object in core's side to prepare path like param=a,1,b,2. @timov how do you think?

https://github.com/Azure/cadl-ranch/blob/main/packages/cadl-ranch-specs/http/routes/main.tsp#L374-L380

image

Copy link
Member Author

Choose a reason for hiding this comment

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

I re-thinked this a little and it would be problematic if we convert any object to format like a,1,b,2 in core e.g if this is a Date, toString would be date string but we may not know how to format as form style. maybe here we should always generate it explictly in codegen.
As interface Param { value: Record<string, string>; explode: false; style: "form" }.

Copy link
Member

Choose a reason for hiding this comment

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

Honestly it feels strange to me that this is considered a valid case. I don't really understand why we would serialize an object in this way. Is there a reference (specification or something) that describes this particular behavior?

Copy link
Member

Choose a reason for hiding this comment

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

Never mind, I see it described here: https://swagger.io/docs/specification/v3_0/describing-parameters/. We can implement that, but like you say there might be a problem for objects where toString actually provides a meaningful value like Date. Do you know if there are any existing cases where RLC generates a type like that for a query parameter?

Copy link
Member

Choose a reason for hiding this comment

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

How about, if the object has a custom toString implementation, use that, otherwise expand it?

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.

Support explode for query in RLC Support allowReserved in path for RLC
2 participants