Skip to content
Merged
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 @@ -197,6 +197,7 @@ function fromSdkModelType(
discriminatorValue: modelType.discriminatorValue,
decorators: decorators,
external: fromSdkExternalTypeInfo(modelType),
serializationOptions: modelType.serializationOptions,
} as InputModelType;

sdkContext.__typeCache.updateSdkTypeReferences(modelType, inputModelType);
Expand Down
158 changes: 158 additions & 0 deletions packages/http-client-csharp/emitter/test/Unit/model-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -937,3 +937,161 @@ describe("Usage decorator on enums", () => {
strictEqual(colorEnum.values.length, 3);
});
});

describe("XML serialization options", () => {
let runner: TestHost;
beforeEach(async () => {
runner = await createEmitterTestHost();
});

it("Model and property XML serializationOptions should be parsed correctly with XML content type operation", async function () {
const program = await typeSpecCompile(
`
@name("XmlBook")
model Book {
@attribute
id: int32;

@name("BookName")
title: string;

@unwrapped
authors: string[];

content: string;
}

@route("/books")
@post
op createBook(@header contentType: "application/xml", @body book: Book): Book;
`,
runner,
{ IsTCGCNeeded: true, IsXmlNeeded: true },
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);
const models = root.models;

const bookModel = models.find((m) => m.name === "Book");
ok(bookModel);
ok(bookModel.serializationOptions);
ok(bookModel.serializationOptions.xml);
strictEqual(bookModel.serializationOptions.xml.name, "XmlBook");

const idProperty = bookModel.properties.find((p) => p.name === "id");
ok(idProperty);
ok(idProperty.serializationOptions);
ok(idProperty.serializationOptions.xml);
strictEqual(idProperty.serializationOptions.xml.name, "id");
strictEqual(idProperty.serializationOptions.xml.attribute, true);
strictEqual(idProperty.serializationOptions.xml.unwrapped, false);

const titleProperty = bookModel.properties.find((p) => p.name === "title");
ok(titleProperty);
ok(titleProperty.serializationOptions);
ok(titleProperty.serializationOptions.xml);
strictEqual(titleProperty.serializationOptions.xml.name, "BookName");
strictEqual(titleProperty.serializationOptions.xml.attribute, false);
strictEqual(titleProperty.serializationOptions.xml.unwrapped, false);

const authorsProperty = bookModel.properties.find((p) => p.name === "authors");
ok(authorsProperty);
ok(authorsProperty.serializationOptions);
ok(authorsProperty.serializationOptions.xml);
strictEqual(authorsProperty.serializationOptions.xml.name, "authors");
strictEqual(authorsProperty.serializationOptions.xml.attribute, false);
strictEqual(authorsProperty.serializationOptions.xml.unwrapped, true);

const contentProperty = bookModel.properties.find((p) => p.name === "content");
ok(contentProperty);
ok(contentProperty.serializationOptions);
ok(contentProperty.serializationOptions.xml);
strictEqual(contentProperty.serializationOptions.xml.name, "content");
strictEqual(contentProperty.serializationOptions.xml.attribute, false);
strictEqual(contentProperty.serializationOptions.xml.unwrapped, false);
});

it("Property with @name decorator should have correct serializedName from XML options", async function () {
const program = await typeSpecCompile(
`
model XmlModel {
@name("CustomElementName")
elementValue: string;

@attribute
@name("attr")
attributeValue: int32;
}

@route("/xml")
@post
op sendXml(@header contentType: "application/xml", @body data: XmlModel): void;
`,
runner,
{ IsTCGCNeeded: true, IsXmlNeeded: true },
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);
const models = root.models;

const xmlModel = models.find((m) => m.name === "XmlModel");
ok(xmlModel);

const elementProperty = xmlModel.properties.find((p) => p.name === "elementValue");
ok(elementProperty);
strictEqual(elementProperty.serializedName, "CustomElementName");
ok(elementProperty.serializationOptions);
ok(elementProperty.serializationOptions.xml);
strictEqual(elementProperty.serializationOptions.xml.name, "CustomElementName");
strictEqual(elementProperty.serializationOptions.xml.attribute, false);

const attrProperty = xmlModel.properties.find((p) => p.name === "attributeValue");
ok(attrProperty);
strictEqual(attrProperty.serializedName, "attr");
ok(attrProperty.serializationOptions);
ok(attrProperty.serializationOptions.xml);
strictEqual(attrProperty.serializationOptions.xml.name, "attr");
strictEqual(attrProperty.serializationOptions.xml.attribute, true);
});

it("Array property should have itemsName in XML serializationOptions", async function () {
const program = await typeSpecCompile(
`
model Item {
name: string;
}

model Container {
items: Item[];
}

@route("/container")
@post
op sendContainer(@header contentType: "application/xml", @body container: Container): void;
`,
runner,
{ IsTCGCNeeded: true, IsXmlNeeded: true },
);

const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);
const models = root.models;

const containerModel = models.find((m) => m.name === "Container");
ok(containerModel);

// Validate items property has itemsName
const itemsProperty = containerModel.properties.find((p) => p.name === "items");
ok(itemsProperty);
ok(itemsProperty.serializationOptions);
ok(itemsProperty.serializationOptions.xml);
strictEqual(itemsProperty.serializationOptions.xml.name, "items");
ok(itemsProperty.serializationOptions.xml.itemsName);
strictEqual(itemsProperty.serializationOptions.xml.itemsName, "Item");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,13 @@ internal set

_discriminatedSubtypes = new Dictionary<string, InputModelType>(value);

InputModelTypeUsage usage = Usage;
if (!usage.HasFlag(InputModelTypeUsage.Xml))
{
usage |= InputModelTypeUsage.Json;
}
var cleanBaseName = Name.ToIdentifierName();

_discriminatedSubtypes.Add(UnknownDiscriminatorValue,
new InputModelType(
$"Unknown{cleanBaseName}",
Expand All @@ -108,7 +114,7 @@ internal set
null,
null,
$"Unknown variant of {cleanBaseName}",
Usage | InputModelTypeUsage.Json,
usage,
[],
this,
[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,11 @@ internal static InputModelType CreateModelType(ref Utf8JsonReader reader, string
model.Doc = doc;
var parsedUsage = Enum.TryParse<InputModelTypeUsage>(usageString, ignoreCase: true, out var usage) ? usage : InputModelTypeUsage.None;

// All models are given a usage of JSON so that they can be persisted regardless of whether
// they are used in requests or responses.
model.Usage = parsedUsage | InputModelTypeUsage.Json;
if (!parsedUsage.HasFlag(InputModelTypeUsage.Xml))
{
parsedUsage |= InputModelTypeUsage.Json;
}
model.Usage = parsedUsage;
model.DiscriminatorValue = discriminatorValue;
model.DiscriminatorProperty = discriminatorProperty;
model.AdditionalProperties = additionalProperties;
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.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
Expand Down Expand Up @@ -117,5 +118,153 @@ public void IsDynamicModelPropagatesWithDiscriminator()
Assert.IsNotNull(unknownFooModel);
Assert.IsTrue(unknownFooModel!.IsDynamicModel, "UnknownFoo model should be marked as dynamic when base is dynamic");
}

[Test]
public void XmlSerializationOptionsAreDeserializedCorrectly()
{
var directory = Helpers.GetAssetFileOrDirectoryPath(false);
var content = File.ReadAllText(Path.Combine(directory, "tspCodeModel.json"));
var referenceHandler = new TypeSpecReferenceHandler();
var options = new JsonSerializerOptions
{
AllowTrailingCommas = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
new InputNamespaceConverter(referenceHandler),
new InputTypeConverter(referenceHandler),
new InputDecoratorInfoConverter(),
new InputModelTypeConverter(referenceHandler),
new InputModelPropertyConverter(referenceHandler),
new InputSerializationOptionsConverter(),
new InputJsonSerializationOptionsConverter(),
new InputXmlSerializationOptionsConverter(),
new InputArrayTypeConverter(referenceHandler),
},
};
var inputNamespace = JsonSerializer.Deserialize<InputNamespace>(content, options);

Assert.IsNotNull(inputNamespace);

var xmlBookModel = inputNamespace!.Models.SingleOrDefault(m => m.Name == "XmlBook");
Assert.IsNotNull(xmlBookModel);
Assert.IsNotNull(xmlBookModel!.SerializationOptions);
Assert.IsNotNull(xmlBookModel.SerializationOptions.Xml);
Assert.AreEqual("XmlBookElement", xmlBookModel.SerializationOptions.Xml!.Name);

var idProperty = xmlBookModel.Properties.SingleOrDefault(p => p.Name == "id");
Assert.IsNotNull(idProperty);
Assert.IsNotNull(idProperty!.SerializationOptions);
Assert.IsNotNull(idProperty.SerializationOptions!.Xml);
Assert.AreEqual("id", idProperty.SerializationOptions.Xml!.Name);
Assert.IsTrue(idProperty.SerializationOptions.Xml.Attribute);
Assert.IsFalse(idProperty.SerializationOptions.Xml.Unwrapped);

var titleProperty = xmlBookModel.Properties.SingleOrDefault(p => p.Name == "title");
Assert.IsNotNull(titleProperty);
Assert.IsNotNull(titleProperty!.SerializationOptions);
Assert.IsNotNull(titleProperty.SerializationOptions!.Xml);
Assert.AreEqual("BookTitle", titleProperty.SerializationOptions.Xml!.Name);
Assert.IsFalse(titleProperty.SerializationOptions.Xml.Attribute);
Assert.IsFalse(titleProperty.SerializationOptions.Xml.Unwrapped);
Assert.AreEqual("BookTitle", titleProperty.SerializedName);

var authorsProperty = xmlBookModel.Properties.SingleOrDefault(p => p.Name == "authors");
Assert.IsNotNull(authorsProperty);
Assert.IsNotNull(authorsProperty!.SerializationOptions);
Assert.IsNotNull(authorsProperty.SerializationOptions!.Xml);
Assert.AreEqual("authors", authorsProperty.SerializationOptions.Xml!.Name);
Assert.IsFalse(authorsProperty.SerializationOptions.Xml.Attribute);
Assert.IsTrue(authorsProperty.SerializationOptions.Xml.Unwrapped);
Assert.AreEqual("author", authorsProperty.SerializationOptions.Xml.ItemsName);

var contentProperty = xmlBookModel.Properties.SingleOrDefault(p => p.Name == "content");
Assert.IsNotNull(contentProperty);
Assert.IsNotNull(contentProperty!.SerializationOptions);
Assert.IsNotNull(contentProperty.SerializationOptions!.Xml);
Assert.AreEqual("content", contentProperty.SerializationOptions.Xml!.Name);
Assert.IsFalse(contentProperty.SerializationOptions.Xml.Attribute);
Assert.IsFalse(contentProperty.SerializationOptions.Xml.Unwrapped);
}

[Test]
public void UnknownDiscriminatedModelWithXmlOnlyUsageDoesNotAddJson()
{
var discriminatorProperty = InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true);
var derivedModel = InputFactory.Model(
"DerivedModel",
usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml,
properties: [InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true)],
discriminatedKind: "derived");

var baseModel = InputFactory.Model(
"BaseModel",
usage: InputModelTypeUsage.Input | InputModelTypeUsage.Xml,
properties: [discriminatorProperty],
discriminatorProperty: discriminatorProperty,
discriminatedModels: new Dictionary<string, InputModelType> { { "derived", derivedModel } });

// Verify the unknown discriminated model was created
Assert.IsTrue(baseModel.DiscriminatedSubtypes.ContainsKey("unknown"), "Unknown discriminated subtype should be created");
var unknownModel = baseModel.DiscriminatedSubtypes["unknown"];

// Validate the unknown model has XML flag but NOT Json flag (since base is XML-only)
Assert.IsTrue(unknownModel.Usage.HasFlag(InputModelTypeUsage.Xml), "Unknown model should have Xml usage flag from base");
Assert.IsFalse(unknownModel.Usage.HasFlag(InputModelTypeUsage.Json), "Unknown model should NOT have Json usage flag when base is XML-only");
Assert.IsTrue(unknownModel.Usage.HasFlag(InputModelTypeUsage.Input), "Unknown model should have Input usage flag from base");
}

[Test]
public void UnknownDiscriminatedModelWithNonXmlUsageAddsJson()
{
var discriminatorProperty = InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true);
var derivedModel = InputFactory.Model(
"DerivedModel",
usage: InputModelTypeUsage.Input | InputModelTypeUsage.Output,
properties: [InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true)],
discriminatedKind: "derived");
var baseModel = InputFactory.Model(
"BaseModel",
usage: InputModelTypeUsage.Input | InputModelTypeUsage.Output,
properties: [discriminatorProperty],
discriminatorProperty: discriminatorProperty,
discriminatedModels: new Dictionary<string, InputModelType> { { "derived", derivedModel } });

// Verify the unknown discriminated model was created
Assert.IsTrue(baseModel.DiscriminatedSubtypes.ContainsKey("unknown"), "Unknown discriminated subtype should be created");
var unknownModel = baseModel.DiscriminatedSubtypes["unknown"];

// Validate the unknown model has Json flag added (since base is non-XML)
Assert.IsTrue(unknownModel.Usage.HasFlag(InputModelTypeUsage.Json), "Unknown model should have Json usage flag added when base is non-XML");
Assert.IsFalse(unknownModel.Usage.HasFlag(InputModelTypeUsage.Xml), "Unknown model should NOT have Xml usage flag");
Assert.IsTrue(unknownModel.Usage.HasFlag(InputModelTypeUsage.Input), "Unknown model should have Input usage flag from base");
Assert.IsTrue(unknownModel.Usage.HasFlag(InputModelTypeUsage.Output), "Unknown model should have Output usage flag from base");
}

[Test]
public void UnknownDiscriminatedModelWithBothXmlAndJsonUsageRetainsBoth()
{
var discriminatorProperty = InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true);
var derivedModel = InputFactory.Model(
"DerivedModel",
usage: InputModelTypeUsage.Input | InputModelTypeUsage.Output | InputModelTypeUsage.Xml | InputModelTypeUsage.Json,
properties: [InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true)],
discriminatedKind: "derived");

var baseModel = InputFactory.Model(
"BaseModel",
usage: InputModelTypeUsage.Input | InputModelTypeUsage.Output | InputModelTypeUsage.Xml | InputModelTypeUsage.Json,
properties: [discriminatorProperty],
discriminatorProperty: discriminatorProperty,
discriminatedModels: new Dictionary<string, InputModelType> { { "derived", derivedModel } });

Assert.IsTrue(baseModel.DiscriminatedSubtypes.ContainsKey("unknown"), "Unknown discriminated subtype should be created");
var unknownModel = baseModel.DiscriminatedSubtypes["unknown"];

Assert.IsTrue(unknownModel.Usage.HasFlag(InputModelTypeUsage.Xml), "Unknown model should have Xml usage flag from base");
Assert.IsTrue(unknownModel.Usage.HasFlag(InputModelTypeUsage.Json), "Unknown model should have Json usage flag from base");
Assert.IsTrue(unknownModel.Usage.HasFlag(InputModelTypeUsage.Input), "Unknown model should have Input usage flag from base");
Assert.IsTrue(unknownModel.Usage.HasFlag(InputModelTypeUsage.Output), "Unknown model should have Output usage flag from base");
}
}
}
Loading
Loading