Skip to content

Commit edaf87f

Browse files
Copilotjorgerangel-msft
authored andcommitted
feat: add multipart/form-data
1 parent 3a90abd commit edaf87f

297 files changed

Lines changed: 13915 additions & 1483 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/http-client-csharp/emitter/src/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ export const _minSupportedDotNetSdkVersion = 8;
2323
* @internal
2424
*/
2525
export const _defaultGeneratorName = "ScmCodeModelGenerator";
26+
27+
/**
28+
* The cross-language definition id for the TypeSpec HTTP File model.
29+
* @internal
30+
*/
31+
export const _httpFileCrossLanguageDefinitionId = "TypeSpec.Http.File";

packages/http-client-csharp/emitter/src/lib/type-converter.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
UsageFlags,
2222
} from "@azure-tools/typespec-client-generator-core";
2323
import { createDiagnosticCollector, Diagnostic, Model, NoTarget } from "@typespec/compiler";
24+
import { _httpFileCrossLanguageDefinitionId } from "../constants.js";
2425
import { CSharpEmitterContext } from "../sdk-context.js";
2526
import {
2627
InputArrayType,
@@ -216,6 +217,7 @@ function fromSdkModelType(
216217
external: fromSdkExternalTypeInfo(modelType),
217218
serializationOptions: modelType.serializationOptions,
218219
isExactName: modelType.isExactName,
220+
isFileType: modelType.crossLanguageDefinitionId === _httpFileCrossLanguageDefinitionId,
219221
} as InputModelType;
220222

221223
sdkContext.__typeCache.updateSdkTypeReferences(modelType, inputModelType);
@@ -295,6 +297,17 @@ function fromSdkModelProperty(
295297
isExactName: sdkProperty.isExactName,
296298
} as InputModelProperty;
297299

300+
if (sdkProperty.serializationOptions?.multipart?.isFilePart === true) {
301+
if (property.type.kind === "model" || property.type.kind === "bytes") {
302+
property.type.isFileType = true;
303+
} else if (
304+
property.type.kind === "array" &&
305+
(property.type.valueType.kind === "model" || property.type.valueType.kind === "bytes")
306+
) {
307+
property.type.valueType.isFileType = true;
308+
}
309+
}
310+
298311
if (property) {
299312
sdkContext.__typeCache.updateSdkPropertyReferences(sdkProperty, property);
300313
}

packages/http-client-csharp/emitter/src/type/input-type.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export interface InputPrimitiveType extends InputTypeBase {
8888
encode?: string; // In TCGC this is required, and when there is no encoding, it just has the same value as kind
8989
crossLanguageDefinitionId: string;
9090
baseType?: InputPrimitiveType;
91+
isFileType?: boolean;
9192
}
9293

9394
export interface InputLiteralType extends InputTypeBase {
@@ -166,6 +167,7 @@ export interface InputModelType extends InputTypeBase {
166167
serializationOptions: SerializationOptions;
167168
/** Whether the name should be used exactly as-is, without casing transformations. */
168169
isExactName?: boolean;
170+
isFileType: boolean;
169171
}
170172

171173
export interface InputPropertyTypeBase extends DecoratedType {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
using Microsoft.TypeSpec.Generator.Input;
6+
7+
namespace Microsoft.TypeSpec.Generator.ClientModel.Primitives
8+
{
9+
/// <summary>
10+
/// Represents multipart/form-data serialization options for a property.
11+
/// </summary>
12+
public class MultipartSerialization
13+
{
14+
public MultipartSerialization(InputMultipartOptions options)
15+
{
16+
Name = options.Name;
17+
IsFilePart = options.IsFilePart;
18+
IsMulti = options.IsMulti;
19+
DefaultContentTypes = options.DefaultContentTypes;
20+
Filename = options.Filename;
21+
ContentType = options.ContentType;
22+
}
23+
24+
/// <summary> Gets the serialized name of the part. </summary>
25+
public string Name { get; }
26+
27+
/// <summary> Gets a value indicating whether the part represents a file. </summary>
28+
public bool IsFilePart { get; }
29+
30+
/// <summary> Gets a value indicating whether the part represents a collection of files. </summary>
31+
public bool IsMulti { get; }
32+
33+
/// <summary> Gets the default media types declared for this part. </summary>
34+
public IReadOnlyList<string> DefaultContentTypes { get; }
35+
36+
/// <summary> Gets the file's filename property, if any. </summary>
37+
public InputModelProperty? Filename { get; }
38+
39+
/// <summary> Gets the file's content type property, if any. </summary>
40+
public InputModelProperty? ContentType { get; }
41+
}
42+
}

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmKnownParameters.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,24 @@ public static ParameterProvider ClientOptions(CSharpType clientOptionsType)
6262
DefaultValue = Static(typeof(DateTimeOffset)).Property(nameof(DateTimeOffset.Now))
6363
};
6464

65-
public static readonly ParameterProvider ContentType = new("contentType", $"The contentType to use which has the multipart/form-data boundary.", typeof(string), wireInfo: new PropertyWireInformation(SerializationFormat.Default, true, false, false, false, "Content-Type", false, false));
65+
private const string ContentTypeParameterName = "contentType";
66+
private static readonly FormattableString ContentTypeDescription = $"The contentType to use which has the multipart/form-data boundary.";
67+
private static readonly PropertyWireInformation ContentTypeWireInfo = new(SerializationFormat.Default, true, false, false, false, "Content-Type", false, false);
68+
69+
public static readonly ParameterProvider ContentType = new(
70+
ContentTypeParameterName,
71+
ContentTypeDescription,
72+
typeof(string),
73+
wireInfo: ContentTypeWireInfo)
74+
{
75+
Validation = ParameterValidationType.AssertNotNullOrEmpty,
76+
};
77+
78+
public static readonly ParameterProvider OptionalContentType = new(
79+
ContentTypeParameterName,
80+
ContentTypeDescription,
81+
typeof(string),
82+
wireInfo: ContentTypeWireInfo);
6683

6784
public static readonly ParameterProvider NextPage =
6885
new ParameterProvider("nextPage", $"The url of the next page of responses.", typeof(Uri));

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Primitives/ScmSerializationOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ public ScmSerializationOptions(InputSerializationOptions inputSerializationOptio
1616
Xml = inputSerializationOptions.Xml != null
1717
? new(inputSerializationOptions.Xml)
1818
: null;
19+
Multipart = inputSerializationOptions.Multipart != null
20+
? new(inputSerializationOptions.Multipart)
21+
: null;
1922
}
2023

2124
public JsonSerialization? Json { get; }
2225

2326
public XmlSerialization? Xml { get; }
27+
28+
public MultipartSerialization? Multipart { get; }
2429
}
2530
}

packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ModelSerializationExtensionsDefinition.Xml.cs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.ClientModel;
56
using System.ClientModel.Primitives;
7+
using System.Collections.Generic;
68
using System.IO;
79
using System.Xml;
810
using System.Xml.Linq;
@@ -14,6 +16,7 @@
1416
using Microsoft.TypeSpec.Generator.Statements;
1517
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;
1618

19+
#pragma warning disable SCME0004 // FileBinaryContent is evaluation-only.
1720
namespace Microsoft.TypeSpec.Generator.ClientModel.Providers
1821
{
1922
public sealed partial class ModelSerializationExtensionsDefinition
@@ -256,7 +259,78 @@ private MethodProvider BuildXmlWriteObjectValueMethodProvider()
256259
var defaultCase = SwitchCaseStatement.Default(
257260
Throw(New.NotSupportedException(new FormattableStringExpression("Not supported type {0}", [new TypeOfExpression(_t)]))));
258261

259-
var body = new SwitchStatement(value, [persistableModelCase, defaultCase]);
262+
var cases = new List<SwitchCaseStatement> { persistableModelCase };
263+
264+
if (HasFileBinaryContentXmlModel)
265+
{
266+
var fileBinaryContentCase = new SwitchCaseStatement(
267+
Declare("fileBinaryContent", typeof(FileBinaryContent), out var fileBinaryContentVar),
268+
new MethodBodyStatement[]
269+
{
270+
writer.Invoke(WriteFileBinaryContentMethodName, fileBinaryContentVar).Terminate(),
271+
Return()
272+
});
273+
cases.Add(fileBinaryContentCase);
274+
}
275+
276+
cases.Add(defaultCase);
277+
278+
var body = new SwitchStatement(value, cases);
279+
280+
return new MethodProvider(signature, body, this, XmlDocProvider.Empty);
281+
}
282+
283+
private MethodProvider BuildWriteFileBinaryContentXmlMethodProvider()
284+
{
285+
var valueParameter = new ParameterProvider("value", FormattableStringHelpers.Empty, typeof(FileBinaryContent));
286+
var signature = new MethodSignature(
287+
Name: WriteFileBinaryContentMethodName,
288+
Description: null,
289+
Modifiers: _methodModifiers,
290+
ReturnType: null,
291+
ReturnDescription: null,
292+
Parameters: [_xmlWriterParameter, valueParameter]);
293+
294+
var writer = _xmlWriterParameter.As<XmlWriter>();
295+
var value = valueParameter.As<FileBinaryContent>();
296+
297+
// value.TryComputeLength(out long length)
298+
var tryComputeLength = value.TryComputeLength(out var lengthVariable);
299+
var length = lengthVariable.As<long>();
300+
301+
// length <= int.MaxValue
302+
var fitsInInt = new BinaryOperatorExpression("<=", length, IntSnippets.MaxValue).As<bool>();
303+
304+
// value.TryComputeLength(out long length) && length <= int.MaxValue ? (int)length : 0
305+
var capacityExpression = new TernaryConditionalExpression(
306+
tryComputeLength.And(fitsInInt),
307+
length.CastTo(typeof(int)),
308+
Literal(0));
309+
310+
var declareCapacity = Declare("capacity", typeof(int), capacityExpression, out var capacity);
311+
312+
// using MemoryStream stream = new MemoryStream(capacity);
313+
var declareStream = UsingDeclare(
314+
"stream",
315+
typeof(MemoryStream),
316+
New.Instance<MemoryStream>(capacity),
317+
out var stream);
318+
var streamScoped = stream.As<Stream>();
319+
320+
// value.WriteTo(stream);
321+
var writeTo = value.WriteTo(streamScoped).Terminate();
322+
323+
// writer.WriteBase64(stream.GetBuffer(), 0, (int)stream.Position);
324+
var positionAsInt = streamScoped.Position().CastTo(typeof(int));
325+
var writeBase64 = writer.WriteBase64(streamScoped.GetBuffer(), Literal(0), positionAsInt);
326+
327+
var body = new MethodBodyStatement[]
328+
{
329+
declareCapacity,
330+
declareStream,
331+
writeTo,
332+
writeBase64,
333+
};
260334

261335
return new MethodProvider(signature, body, this, XmlDocProvider.Empty);
262336
}

0 commit comments

Comments
 (0)