Skip to content

http-client-java, xml support in core-v2 #7788

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
54 changes: 38 additions & 16 deletions packages/http-client-java/emitter/src/code-model-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import {
Client as CodeModelClient,
EncodedSchema,
PageableContinuationToken,
Serializable,
} from "./common/client.js";
import { CodeModel } from "./common/code-model.js";
import { LongRunningMetadata } from "./common/long-running-metadata.js";
Expand Down Expand Up @@ -146,6 +147,7 @@ import {
getPropertySerializedName,
getUnionDescription,
getUsage,
getXmlSerializationFormat,
modelIs,
pushDistinct,
} from "./type-utils.js";
Expand Down Expand Up @@ -2705,45 +2707,51 @@ export class CodeModelBuilder {
}
}

// xml
if (type.serializationOptions.xml) {
objectSchema.serialization = objectSchema.serialization ?? {};
objectSchema.serialization.xml = getXmlSerializationFormat(type);
}

return objectSchema;
}

private processModelProperty(prop: SdkModelPropertyType): Property {
private processModelProperty(modelProperty: SdkModelPropertyType): Property {
let nullable = false;
let nonNullType = prop.type;
let nonNullType = modelProperty.type;
if (nonNullType.kind === "nullable") {
nullable = true;
nonNullType = nonNullType.type;
}
let schema;

let extensions: Record<string, any> | undefined = undefined;
if (this.isSecret(prop)) {
if (this.isSecret(modelProperty)) {
extensions = extensions ?? {};
extensions["x-ms-secret"] = true;
// if the property does not return in response, it had to be nullable
nullable = true;
}
if (prop.kind === "property" && prop.flatten) {
if (modelProperty.kind === "property" && modelProperty.flatten) {
extensions = extensions ?? {};
extensions["x-ms-client-flatten"] = true;
}
const mutability = this.getMutability(prop);
const mutability = this.getMutability(modelProperty);
if (mutability) {
extensions = extensions ?? {};
extensions["x-ms-mutability"] = mutability;
}

if (prop.kind === "property" && prop.serializationOptions.multipart) {
if (prop.serializationOptions.multipart?.isFilePart) {
schema = this.processMultipartFormDataFilePropertySchema(prop);
if (modelProperty.kind === "property" && modelProperty.serializationOptions.multipart) {
if (modelProperty.serializationOptions.multipart?.isFilePart) {
schema = this.processMultipartFormDataFilePropertySchema(modelProperty);
} else if (
prop.type.kind === "model" &&
prop.type.properties.some((it) => it.kind === "body")
modelProperty.type.kind === "model" &&
modelProperty.type.properties.some((it) => it.kind === "body")
) {
// TODO: this is HttpPart of non-File. TCGC should help handle this.
schema = this.processSchema(
prop.type.properties.find((it) => it.kind === "body")!.type,
modelProperty.type.properties.find((it) => it.kind === "body")!.type,
"",
);
} else {
Expand All @@ -2753,14 +2761,28 @@ export class CodeModelBuilder {
schema = this.processSchema(nonNullType, "");
}

return new Property(prop.name, prop.doc ?? "", schema, {
summary: prop.summary,
required: !prop.optional,
const codeModelProperty = new Property(modelProperty.name, modelProperty.doc ?? "", schema, {
summary: modelProperty.summary,
required: !modelProperty.optional,
nullable: nullable,
readOnly: this.isReadOnly(prop),
serializedName: prop.kind === "property" ? getPropertySerializedName(prop) : undefined,
readOnly: this.isReadOnly(modelProperty),
serializedName:
modelProperty.kind === "property" ? getPropertySerializedName(modelProperty) : undefined,
extensions: extensions,
});

// xml
if (modelProperty.kind === "property" && modelProperty.serializationOptions.xml) {
// property.serializedName is set via getPropertySerializedName

// "serialization" is set to the property in TypeSpec emitter, not in the schema
// this avoid duplicate schema, when different property has different serialization options, but refers to the same schema
const propertyWithSerialization = codeModelProperty as Serializable;
propertyWithSerialization.serialization = propertyWithSerialization.serialization ?? {};
propertyWithSerialization.serialization.xml = getXmlSerializationFormat(modelProperty);
}

return codeModelProperty;
}

private processUnionSchema(type: SdkUnionType, name: string): Schema {
Expand Down
10 changes: 10 additions & 0 deletions packages/http-client-java/emitter/src/common/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Security,
} from "@autorest/codemodel";
import { DeepPartial } from "@azure-tools/codegen";
import { XmlSerializationFormat } from "./formats/xml.js";

export interface Client extends Aspect {
/** All operations */
Expand Down Expand Up @@ -124,3 +125,12 @@ export class PageableContinuationToken {
this.responseHeader = responseHeader;
}
}

export interface Serializable {
/**
* The serialization format for the type or property.
*/
serialization?: {
xml?: XmlSerializationFormat;
};
}
10 changes: 10 additions & 0 deletions packages/http-client-java/emitter/src/common/formats/xml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SerializationFormat } from "@autorest/codemodel";

export interface XmlSerializationFormat extends SerializationFormat {
name?: string;
namespace?: string;
prefix?: string;
attribute: boolean;
wrapped: boolean;
text: boolean;
}
40 changes: 40 additions & 0 deletions packages/http-client-java/emitter/src/type-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
isTemplateInstance,
isTypeSpecValueTypeOf,
} from "@typespec/compiler";
import { XmlSerializationFormat } from "./common/formats/xml.js";
import { DurationSchema } from "./common/schemas/time.js";
import { SchemaContext } from "./common/schemas/usage.js";
import { getNamespace } from "./utils.js";
Expand Down Expand Up @@ -345,16 +346,55 @@ export function isArmCommonType(entity: Type): boolean {
return false;
}

/**
* Get the serialized name of a property, based on either JSON, or XML, or Multipart.
*
* @param property the model property.
* @returns the serialized name of the property.
*/
export function getPropertySerializedName(property: SdkBodyModelPropertyType): string {
// still fallback to "property.name", as for orphan model, serializationOptions.json is undefined
return (
property.serializationOptions.json?.name ??
property.serializationOptions.xml?.name ??
property.serializationOptions.multipart?.name ??
property.__raw?.name ??
property.name
);
}

/**
* Get the XML serialization format for a type or property.
*
* @param type the type or model property.
* @returns the XML serialization format, or undefined if not applicable.
*/
export function getXmlSerializationFormat(
type: SdkModelType | SdkBodyModelPropertyType,
): XmlSerializationFormat | undefined {
if (!type.serializationOptions.xml) {
return undefined;
}
// "unwrapped" from xml lib can be applied to both array and string
let propertyTypeIsArray = false;
let propertyTypeIsText = false;
if (type.kind === "property") {
propertyTypeIsArray = type.type.kind === "array";
propertyTypeIsText =
type.type.kind !== "array" && type.type.kind !== "dict" && type.type.kind !== "model";
}
// name, namespace and prefix on type and property
// attribute, wrapped, text on property
return {
name: type.serializationOptions.xml.name ?? undefined,
namespace: type.serializationOptions.xml.ns?.namespace ?? undefined,
prefix: type.serializationOptions.xml.ns?.prefix ?? undefined,
attribute: type.serializationOptions.xml.attribute ?? false,
wrapped: propertyTypeIsArray ? !(type.serializationOptions.xml.unwrapped ?? true) : false,
text: propertyTypeIsText ? (type.serializationOptions.xml.unwrapped ?? false) : false,
};
}

function getDecoratorScopedValue<T>(
type: DecoratedType,
decorator: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,6 @@ try {

Write-Host "Copied http-specs to current directory"

# remove xml tests, emitter has not supported xml model
Remove-Item ./specs/payload/xml -Recurse -Force

$job = (Get-ChildItem ./specs -Include "main.tsp","old.tsp" -File -Recurse) | ForEach-Object -Parallel $generateScript -ThrottleLimit $Parallelization -AsJob

$job | Wait-Job -Timeout 1200
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package payload.xml;

import io.clientcore.core.annotations.Metadata;
import io.clientcore.core.annotations.MetadataProperties;
import io.clientcore.core.serialization.xml.XmlReader;
import io.clientcore.core.serialization.xml.XmlSerializable;
import io.clientcore.core.serialization.xml.XmlToken;
import io.clientcore.core.serialization.xml.XmlWriter;
import java.util.ArrayList;
import java.util.List;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;

/**
* Contains an array of models.
*/
@Metadata(properties = { MetadataProperties.IMMUTABLE })
public final class ModelWithArrayOfModel implements XmlSerializable<ModelWithArrayOfModel> {
/*
* The items property.
*/
@Metadata(properties = { MetadataProperties.GENERATED })
private final List<SimpleModel> items;

/**
* Creates an instance of ModelWithArrayOfModel class.
*
* @param items the items value to set.
*/
@Metadata(properties = { MetadataProperties.GENERATED })
public ModelWithArrayOfModel(List<SimpleModel> items) {
this.items = items;
}

/**
* Get the items property: The items property.
*
* @return the items value.
*/
@Metadata(properties = { MetadataProperties.GENERATED })
public List<SimpleModel> getItems() {
return this.items;
}

@Metadata(properties = { MetadataProperties.GENERATED })
@Override
public XmlWriter toXml(XmlWriter xmlWriter) throws XMLStreamException {
return toXml(xmlWriter, null);
}

@Metadata(properties = { MetadataProperties.GENERATED })
@Override
public XmlWriter toXml(XmlWriter xmlWriter, String rootElementName) throws XMLStreamException {
rootElementName
= rootElementName == null || rootElementName.isEmpty() ? "ModelWithArrayOfModel" : rootElementName;
xmlWriter.writeStartElement(rootElementName);
if (this.items != null) {
xmlWriter.writeStartElement("items");
for (SimpleModel element : this.items) {
xmlWriter.writeXml(element, "SimpleModel");
}
xmlWriter.writeEndElement();
}
return xmlWriter.writeEndElement();
}

/**
* Reads an instance of ModelWithArrayOfModel from the XmlReader.
*
* @param xmlReader The XmlReader being read.
* @return An instance of ModelWithArrayOfModel if the XmlReader was pointing to an instance of it, or null if it
* was pointing to XML null.
* @throws IllegalStateException If the deserialized XML object was missing any required properties.
* @throws XMLStreamException If an error occurs while reading the ModelWithArrayOfModel.
*/
@Metadata(properties = { MetadataProperties.GENERATED })
public static ModelWithArrayOfModel fromXml(XmlReader xmlReader) throws XMLStreamException {
return fromXml(xmlReader, null);
}

/**
* Reads an instance of ModelWithArrayOfModel from the XmlReader.
*
* @param xmlReader The XmlReader being read.
* @param rootElementName Optional root element name to override the default defined by the model. Used to support
* cases where the model can deserialize from different root element names.
* @return An instance of ModelWithArrayOfModel if the XmlReader was pointing to an instance of it, or null if it
* was pointing to XML null.
* @throws IllegalStateException If the deserialized XML object was missing any required properties.
* @throws XMLStreamException If an error occurs while reading the ModelWithArrayOfModel.
*/
@Metadata(properties = { MetadataProperties.GENERATED })
public static ModelWithArrayOfModel fromXml(XmlReader xmlReader, String rootElementName) throws XMLStreamException {
String finalRootElementName
= rootElementName == null || rootElementName.isEmpty() ? "ModelWithArrayOfModel" : rootElementName;
return xmlReader.readObject(finalRootElementName, reader -> {
List<SimpleModel> items = null;
while (reader.nextElement() != XmlToken.END_ELEMENT) {
QName elementName = reader.getElementName();

if ("items".equals(elementName.getLocalPart())) {
while (reader.nextElement() != XmlToken.END_ELEMENT) {
elementName = reader.getElementName();
if ("SimpleModel".equals(elementName.getLocalPart())) {
if (items == null) {
items = new ArrayList<>();
}
items.add(SimpleModel.fromXml(reader, "SimpleModel"));
} else {
reader.skipElement();
}
}
} else {
reader.skipElement();
}
}
return new ModelWithArrayOfModel(items);
});
}
}
Loading
Loading