Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ function fromBodyParameter(
readOnly: isReadOnly(p),
crossLanguageDefinitionId: p.crossLanguageDefinitionId,
methodParameterSegments: diagnostics.pipe(getMethodParameterSegments(sdkContext, p)),
serializationOptions: p.serializationOptions,
};

sdkContext.__typeCache.updateSdkOperationParameterReferences(p, retVar);
Expand Down Expand Up @@ -706,6 +707,7 @@ export function fromSdkHttpOperationResponse(
isErrorResponse:
sdkResponse.type !== undefined && isErrorModel(sdkContext.program, sdkResponse.type.__raw!),
contentTypes: sdkResponse.contentTypes,
serializationOptions: sdkResponse.serializationOptions,
};

sdkContext.__typeCache.updateSdkResponseReferences(sdkResponse, retVar);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export interface InputBodyParameter extends InputPropertyTypeBase {
scope: InputParameterScope;
serializedName: string;
methodParameterSegments?: InputMethodParameter[];
serializationOptions: SerializationOptions;
}

export interface InputEndpointParameter extends InputPropertyTypeBase {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

import { SerializationOptions } from "@azure-tools/typespec-client-generator-core";
import { HttpResponseHeader } from "./http-response-header.js";
import { InputType } from "./input-type.js";

Expand All @@ -10,4 +11,5 @@ export interface OperationResponse {
headers: HttpResponseHeader[];
contentTypes?: string[];
isErrorResponse: boolean;
serializationOptions: SerializationOptions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TestHost } from "@typespec/compiler/testing";
import assert, { deepStrictEqual, ok, strictEqual } from "assert";
import { beforeEach, describe, it, vi } from "vitest";
import { createModel } from "../../src/lib/client-model-builder.js";
import { InputModelType } from "../../src/type/input-type.js";
import {
createCSharpSdkContext,
createEmitterContext,
Expand Down Expand Up @@ -1094,4 +1095,72 @@ describe("XML serialization options", () => {
ok(itemsProperty.serializationOptions.xml.itemsName);
strictEqual(itemsProperty.serializationOptions.xml.itemsName, "Item");
});

it("Body parameter with file payload should have binary serializationOptions populated on the body type", async function () {
const program = await typeSpecCompile(
`
model RawData extends File {
contentType: "application/octet-stream";
contents: bytes;
}

@route("/upload")
@post
op uploadRawData(@bodyRoot data: RawData): void;
`,
runner,
{ IsTCGCNeeded: true },
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);

const method = root.clients[0].methods[0];
ok(method);
const bodyParam = method.operation.parameters.find((p) => p.kind === "body");
ok(bodyParam);
// The body parameter itself always has serializationOptions (tcgc populates
// json/xml options from content types; for a binary file body neither is set).
ok(bodyParam.serializationOptions);
strictEqual(bodyParam.serializationOptions.json, undefined);
Comment thread
jorgerangel-msft marked this conversation as resolved.
strictEqual(bodyParam.serializationOptions.xml, undefined);
// The body's model type carries the binary serialization options.
const bodyType = bodyParam.type as InputModelType;
ok(bodyType.serializationOptions);
ok(bodyType.serializationOptions.binary);
strictEqual(bodyType.serializationOptions.binary.isFile, true);
// bytes contents → not text
strictEqual(bodyType.serializationOptions.binary.isText, false);
// contentTypes should be populated from the model's contentType property
ok(bodyType.serializationOptions.binary.contentTypes);
strictEqual(bodyType.serializationOptions.binary.contentTypes.length, 1);
strictEqual(bodyType.serializationOptions.binary.contentTypes[0], "application/octet-stream");
// filename should be populated for an Http.File-derived model
ok(bodyType.serializationOptions.binary.filename);
strictEqual(bodyType.serializationOptions.binary.filename.name, "filename");
});

it("Body parameter with JSON content type should have json serializationOptions populated", async function () {
Comment thread
jorgerangel-msft marked this conversation as resolved.
const program = await typeSpecCompile(
`
@route("/messages")
@post
op sendMessage(@header contentType: "application/json", @body message: string): void;
`,
runner,
{ IsTCGCNeeded: true },
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);

const method = root.clients[0].methods[0];
ok(method);
const bodyParam = method.operation.parameters.find((p) => p.kind === "body");
ok(bodyParam);
ok(bodyParam.serializationOptions);
ok(bodyParam.serializationOptions.json);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1380,9 +1380,9 @@ private bool TryGetXmlCollectionNamesForResponse(
rootName = null;
childName = null;

// Check if the response uses XML content type
// Check if the response is serialized as XML using the propagated serialization options
var response = ServiceMethod.Operation.Responses.FirstOrDefault(r => !r.IsErrorResponse);
if (response == null || !response.ContentTypes.Any(c => c.Contains(XmlMediaType, StringComparison.OrdinalIgnoreCase)))
if (response?.SerializationOptions?.Xml is null)
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1924,7 +1924,12 @@ public void ConvenienceMethod_XmlListResponse_UsesXDocumentDeserialization()
var operation = InputFactory.Operation(
"GetFoo",
httpMethod: "GET",
responses: [InputFactory.OperationResponse([200], bodytype: arrayType, contentTypes: ["application/xml"])]);
responses: [InputFactory.OperationResponse(
[200],
bodytype: arrayType,
contentTypes: ["application/xml"],
serializationOptions: InputFactory.Serialization.Options(
xml: InputFactory.Serialization.Xml("SignedIdentifier")))]);

var serviceMethod = InputFactory.BasicServiceMethod(
"GetFoo",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;

namespace Microsoft.TypeSpec.Generator.Input
{
/// <summary>
/// Describes how a body is serialized as a binary payload (e.g. a file or raw stream).
/// </summary>
public class InputBinarySerializationOptions
{
public InputBinarySerializationOptions(bool isFile = false, bool? isText = null, IReadOnlyList<string>? contentTypes = null, InputModelProperty? filename = null)
{
IsFile = isFile;
IsText = isText;
ContentTypes = contentTypes;
Filename = filename;
}

/// <summary>
/// Whether this is a file/stream input.
/// </summary>
public bool IsFile { get; internal set; }

/// <summary>
/// Whether the file contents should be represented as a string or a raw byte stream.
/// Only set when <see cref="IsFile"/> is <c>true</c>.
/// </summary>
public bool? IsText { get; internal set; }

/// <summary>
/// The list of inner media types of the file.
/// Only set when <see cref="IsFile"/> is <c>true</c>.
/// </summary>
public IReadOnlyList<string>? ContentTypes { get; internal set; }

/// <summary>
/// The model property that represents the filename in the file model.
/// Only set when <see cref="IsFile"/> is <c>true</c>.
/// </summary>
public InputModelProperty? Filename { get; internal set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,21 @@ public InputBodyParameter(
InputConstant? defaultValue,
InputParameterScope scope,
IReadOnlyList<string> contentTypes,
string defaultContentType)
string defaultContentType,
InputSerializationOptions? serializationOptions = null)
Comment thread
jorgerangel-msft marked this conversation as resolved.
: base(name, summary, doc, type, isRequired, isReadOnly, access, serializedName, isApiVersion, defaultValue, scope)
{
ContentTypes = contentTypes;
DefaultContentType = defaultContentType;
SerializationOptions = serializationOptions;
}

public IReadOnlyList<string> ContentTypes { get; internal set; }
public string DefaultContentType { get; internal set; }

/// <summary>
/// Options describing how the body is serialized on the wire.
/// </summary>
public InputSerializationOptions? SerializationOptions { get; internal set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ namespace Microsoft.TypeSpec.Generator.Input
/// </summary>
public sealed class InputOperationResponse
{
public InputOperationResponse(IReadOnlyList<int> statusCodes, InputType? bodyType, IReadOnlyList<InputOperationResponseHeader> headers, bool isErrorResponse, IReadOnlyList<string> contentTypes)
public InputOperationResponse(IReadOnlyList<int> statusCodes, InputType? bodyType, IReadOnlyList<InputOperationResponseHeader> headers, bool isErrorResponse, IReadOnlyList<string> contentTypes, InputSerializationOptions? serializationOptions = null)
{
StatusCodes = statusCodes;
BodyType = bodyType;
Headers = headers;
IsErrorResponse = isErrorResponse;
ContentTypes = contentTypes;
SerializationOptions = serializationOptions;
}

public InputOperationResponse() : this(Array.Empty<int>(), null, Array.Empty<InputOperationResponseHeader>(), false, Array.Empty<string>()) { }
Expand All @@ -27,5 +28,10 @@ public InputOperationResponse() : this(Array.Empty<int>(), null, Array.Empty<In
public IReadOnlyList<InputOperationResponseHeader> Headers { get; }
Comment thread
jorgerangel-msft marked this conversation as resolved.
public bool IsErrorResponse { get; }
public IReadOnlyList<string> ContentTypes { get; }

/// <summary>
/// Options describing how the response body is deserialized from the wire.
/// </summary>
public InputSerializationOptions? SerializationOptions { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ namespace Microsoft.TypeSpec.Generator.Input
{
public class InputSerializationOptions
{
public InputSerializationOptions(InputJsonSerializationOptions? json = null, InputXmlSerializationOptions? xml = null, InputMultipartOptions? multipart = null)
public InputSerializationOptions(InputJsonSerializationOptions? json = null, InputXmlSerializationOptions? xml = null, InputMultipartOptions? multipart = null, InputBinarySerializationOptions? binary = null)
{
Json = json;
Xml = xml;
Multipart = multipart;
Binary = binary;
}

public InputJsonSerializationOptions? Json { get; internal set; }

public InputXmlSerializationOptions? Xml { get; internal set; }

public InputMultipartOptions? Multipart { get; internal set; }

public InputBinarySerializationOptions? Binary { get; internal set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Microsoft.TypeSpec.Generator.Input
{
internal sealed class InputBinarySerializationOptionsConverter : JsonConverter<InputBinarySerializationOptions>
{
public InputBinarySerializationOptionsConverter()
{
}

public override InputBinarySerializationOptions Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadInputBinarySerializationOptions(ref reader, options);

public override void Write(Utf8JsonWriter writer, InputBinarySerializationOptions value, JsonSerializerOptions options)
=> throw new NotSupportedException("Writing not supported");

private static InputBinarySerializationOptions ReadInputBinarySerializationOptions(ref Utf8JsonReader reader, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.StartObject)
{
reader.Read();
}

bool isFile = false;
bool? isText = null;
IReadOnlyList<string>? contentTypes = null;
InputModelProperty? filename = null;

while (reader.TokenType != JsonTokenType.EndObject)
{
var isKnownProperty = reader.TryReadBoolean("isFile", ref isFile)
|| reader.TryReadNullableBoolean("isText", ref isText)
|| reader.TryReadComplexType("contentTypes", options, ref contentTypes)
|| reader.TryReadComplexType("filename", options, ref filename);

if (!isKnownProperty)
{
reader.SkipProperty();
}
}

return new InputBinarySerializationOptions(isFile, isText, contentTypes, filename);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ internal static InputBodyParameter ReadInputBodyParameter(ref Utf8JsonReader rea
defaultValue: null,
scope: default,
contentTypes: null!,
defaultContentType: null!);
defaultContentType: null!,
serializationOptions: null);
resolver.AddReference(id, parameter);

string? name = null;
Expand All @@ -63,6 +64,7 @@ internal static InputBodyParameter ReadInputBodyParameter(ref Utf8JsonReader rea
string? defaultContentType = null;
IReadOnlyList<InputDecoratorInfo>? decorators = null;
IReadOnlyList<InputMethodParameter>? methodParameterSegments = null;
InputSerializationOptions? serializationOptions = null;

while (reader.TokenType != JsonTokenType.EndObject)
{
Expand All @@ -81,7 +83,8 @@ internal static InputBodyParameter ReadInputBodyParameter(ref Utf8JsonReader rea
|| reader.TryReadComplexType("contentTypes",options, ref contentTypes)
|| reader.TryReadComplexType("defaultContentType", options, ref defaultContentType)
|| reader.TryReadComplexType("decorators", options, ref decorators)
|| reader.TryReadComplexType("methodParameterSegments", options, ref methodParameterSegments);
|| reader.TryReadComplexType("methodParameterSegments", options, ref methodParameterSegments)
|| reader.TryReadComplexType("serializationOptions", options, ref serializationOptions);

if (!isKnownProperty)
{
Expand All @@ -104,6 +107,7 @@ internal static InputBodyParameter ReadInputBodyParameter(ref Utf8JsonReader rea
parameter.ContentTypes = contentTypes ?? throw new JsonException($"{nameof(InputBodyParameter)} must have a contentTypes.");
parameter.DefaultContentType = defaultContentType ?? throw new JsonException($"{nameof(InputBodyParameter)} must have a defaultContentType.");
parameter.MethodParameterSegments = methodParameterSegments;
parameter.SerializationOptions = serializationOptions;

return parameter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ private InputOperationResponse CreateOperationResponse(ref Utf8JsonReader reader
IReadOnlyList<InputOperationResponseHeader>? headers = null;
bool isErrorResponse = default;
IReadOnlyList<string>? contentTypes = null;
InputSerializationOptions? serializationOptions = null;
while (reader.TokenType != JsonTokenType.EndObject)
{
var isKnownProperty = reader.TryReadComplexType("statusCodes", options, ref statusCodes)
|| reader.TryReadComplexType("bodyType", options, ref bodyType)
|| reader.TryReadComplexType("headers", options, ref headers)
|| reader.TryReadBoolean("isErrorResponse", ref isErrorResponse)
|| reader.TryReadComplexType("contentTypes", options, ref contentTypes);
|| reader.TryReadComplexType("contentTypes", options, ref contentTypes)
|| reader.TryReadComplexType("serializationOptions", options, ref serializationOptions);

if (!isKnownProperty)
{
Expand All @@ -49,7 +51,7 @@ private InputOperationResponse CreateOperationResponse(ref Utf8JsonReader reader
contentTypes ??= [];
headers ??= [];

var result = new InputOperationResponse(statusCodes, bodyType, headers, isErrorResponse, contentTypes);
var result = new InputOperationResponse(statusCodes, bodyType, headers, isErrorResponse, contentTypes, serializationOptions);

return result;
}
Expand Down
Loading
Loading