Skip to content
Open
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
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/http-client-java"
---

Support nested property paths in Azure pageable responses.
1 change: 1 addition & 0 deletions .github/instructions/http-client-java.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ After a compile, inspect `tsp-output/**/code-model.yaml` in the test module to s
6. Regenerate by compiling a spec. Do NOT hardcode the TypeSpec file name — it varies per feature, and sometimes you must author a new `<scenario>/main.tsp` first. The test module's `tspconfig.yaml` already configures the emitter and its output dir, so a plain compile is enough; output goes to `tsp-output/`:
`npx tsp compile <path-to-tsp>`
(Optionally add `--option "@typespec/http-client-java.emitter-output-dir=$PWD/tsp-output/<name>"` to isolate output into a subfolder for an easier diff.)
Do NOT run `Generate.ps1` to test one spec. It regenerates every test client, deletes and rebuilds generated source directories, and is reserved for intentional full regeneration (such as a test-spec dependency update).
7. Verify the generated code under `tsp-output/**/src` is as expected. When the spec corresponds to sources tracked in `src/main/java`, compare against them and, if correct, copy the generated files into `src` (replacing existing files) but EXCLUDE `module-info.java`. Some specs do not map to `src` — in that case just verify the output, without comparing or copying.
8. When the spec maps to `src` and you copied the generated code in, run the tests (`mvn test`, or a targeted `--define "test=<pkg>.<Class>"`). Restart the Spector server if needed (`npm run spector-stop` then `npm run spector-start`). If the spec does not map to `src`, verifying the generated output (step 7) is sufficient.

Expand Down
16 changes: 0 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 @@ -1205,22 +1205,6 @@ export class CodeModelBuilder {
? pageItemsResponseProperty[0].serializedName
: undefined;

if (
this.isAzureV1() &&
(pageItemsResponseProperty === undefined || pageItemsResponseProperty.length > 1)
) {
// TCGC should have verified that pageItems exists

// Azure V1 does not support nested page items
reportDiagnostic(this.program, {
code: "nested-page-items-not-supported",
target:
sdkMethod.response.resultSegments?.[sdkMethod.response.resultSegments.length - 1]
?.__raw ?? NoTarget,
});
return;
}

// nextLink
// TODO: nextLink can also be a response header, similar to "sdkMethod.pagingMetadata.continuationTokenResponseSegments"
const nextLinkResponseProperty = findResponsePropertySegments(
Expand Down
6 changes: 0 additions & 6 deletions packages/http-client-java/emitter/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,6 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`Namespace '${"namespace"}' contains reserved Java keywords, replaced it with '${"processedNamespace"}'.`,
},
},
"nested-page-items-not-supported": {
severity: "warning",
messages: {
default: "Nested pageItems is not supported in Azure V1.",
},
},
"constant-header-in-response-removed": {
severity: "warning",
messages: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,54 @@ public class ClientMethodTemplate extends ClientMethodTemplateBase {
protected ClientMethodTemplate() {
}

private static boolean isXmlPagingResponse(ClientMethod clientMethod, JavaSettings settings) {
return settings.isDataPlaneClient()
&& settings.isAzureV1()
&& clientMethod.getProxyMethod().getRawResponseBodyType().isUsedInXml();
}

private static String xmlPageItemsExpression(ClientMethod clientMethod) {
ModelPropertySegment pageItemsSegment = clientMethod.getMethodPageDetails()
.getPageItemsPropertyReference()
.get(clientMethod.getMethodPageDetails().getPageItemsPropertyReference().size() - 1);
IType pageItemsType = pageItemsSegment.getProperty().getClientType();
if (!(pageItemsType instanceof GenericType)) {
throw new IllegalStateException("XML pageable items must be a list of generated models.");
}
IType[] typeArguments = ((GenericType) pageItemsType).getTypeArguments();
if (typeArguments.length != 1 || !(typeArguments[0] instanceof ClassType)) {
throw new IllegalStateException("XML pageable items must be a list of generated models.");
}

ClassType itemType = (ClassType) typeArguments[0];
String itemElementName = pageItemsSegment.getProperty().getXmlListElementName();
if (itemElementName == null || itemElementName.isEmpty()) {
throw new IllegalStateException("XML pageable item element name is required.");
}

return "getXmlValues(res.getValue(), reader -> { try { return BinaryData.fromObject(" + itemType.getFullName()
+ ".fromXml(reader, " + ClassType.STRING.defaultValueExpression(itemElementName)
+ "), XML_SERIALIZER); } catch (javax.xml.stream.XMLStreamException e) { throw new IllegalStateException(e); } }, "
+ xmlPropertyPath(clientMethod.getMethodPageDetails().getPageItemsPropertyReference(), itemElementName)
+ ")";
}

private static String xmlPropertyPath(List<ModelPropertySegment> propertyReference) {
return propertyReference.stream()
.map(segment -> ClassType.STRING.defaultValueExpression(segment.getProperty().getXmlName()))
.collect(Collectors.joining(", "));
}

private static String xmlPropertyPath(List<ModelPropertySegment> propertyReference, String itemElementName) {
return xmlPropertyPath(propertyReference) + ", " + ClassType.STRING.defaultValueExpression(itemElementName);
}

private static String serializedPropertyPath(List<ModelPropertySegment> propertyReference) {
return propertyReference.stream()
.map(segment -> ClassType.STRING.defaultValueExpression(segment.getProperty().getSerializedName()))
.collect(Collectors.joining(", "));
}

public static ClientMethodTemplate getInstance() {
return INSTANCE;
}
Expand Down Expand Up @@ -985,17 +1033,22 @@ protected void pagedSinglePageResponseConversion(ProxyMethod restAPIMethod, Clie
function.line("res.getRequest(),");
function.line("res.getStatusCode(),");
function.line("res.getHeaders(),");
if (settings.isDataPlaneClient()) {
function.line("getValues(res.getValue(), \"%s\"),",
clientMethod.getMethodPageDetails().getSerializedItemName());
if (isXmlPagingResponse(clientMethod, settings)) {
function.line("%s,", xmlPageItemsExpression(clientMethod));
} else if (settings.isDataPlaneClient()) {
function.line("getValues(res.getValue(), %s),",
serializedPropertyPath(clientMethod.getMethodPageDetails().getPageItemsPropertyReference()));
} else {
function.line("res.getValue().%s(),",
CodeNamer.getModelNamer().modelPropertyGetterName(clientMethod.getMethodPageDetails().getItemName()));
}
if (clientMethod.getMethodPageDetails().nonNullNextLink()) {
if (settings.isDataPlaneClient()) {
function.line("getNextLink(res.getValue(), \"%s\"),",
clientMethod.getMethodPageDetails().getSerializedNextLinkName());
if (isXmlPagingResponse(clientMethod, settings)) {
function.line("getXmlNextLink(res.getValue(), %s),",
xmlPropertyPath(clientMethod.getMethodPageDetails().getNextLinkPropertyReference()));
} else if (settings.isDataPlaneClient()) {
function.line("getNextLink(res.getValue(), %s),",
serializedPropertyPath(clientMethod.getMethodPageDetails().getNextLinkPropertyReference()));
} else {
function.line(nextLinkLine(clientMethod));
}
Expand Down Expand Up @@ -1448,17 +1501,22 @@ protected void generatePagedAsyncSinglePage(ClientMethod clientMethod, JavaType
function.line("res.getRequest(),");
function.line("res.getStatusCode(),");
function.line("res.getHeaders(),");
if (settings.isDataPlaneClient() && settings.isAzureV1()) {
function.line("getValues(res.getValue(), \"%s\"),",
clientMethod.getMethodPageDetails().getSerializedItemName());
if (isXmlPagingResponse(clientMethod, settings)) {
function.line("%s,", xmlPageItemsExpression(clientMethod));
} else if (settings.isDataPlaneClient() && settings.isAzureV1()) {
function.line("getValues(res.getValue(), %s),", serializedPropertyPath(
clientMethod.getMethodPageDetails().getPageItemsPropertyReference()));
} else {
function.line("res.getValue().%s(),", CodeNamer.getModelNamer()
.modelPropertyGetterName(clientMethod.getMethodPageDetails().getItemName()));
}
if (clientMethod.getMethodPageDetails().nonNullNextLink()) {
if (settings.isDataPlaneClient() && settings.isAzureV1()) {
function.line("getNextLink(res.getValue(), \"%s\"),",
clientMethod.getMethodPageDetails().getSerializedNextLinkName());
if (isXmlPagingResponse(clientMethod, settings)) {
function.line("getXmlNextLink(res.getValue(), %s),",
xmlPropertyPath(clientMethod.getMethodPageDetails().getNextLinkPropertyReference()));
} else if (settings.isDataPlaneClient() && settings.isAzureV1()) {
function.line("getNextLink(res.getValue(), %s),", serializedPropertyPath(
clientMethod.getMethodPageDetails().getNextLinkPropertyReference()));
} else {
function.line(nextLinkLine(clientMethod));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.IterableType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.MapType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ModelPropertySegment;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ParameterSynthesizedOrigin;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.PrimitiveType;
import com.microsoft.typespec.http.client.generator.core.model.clientmodel.ProxyMethod;
Expand Down Expand Up @@ -88,42 +89,22 @@ protected static void generateProtocolMethodJavadoc(ClientMethod clientMethod, J
if (JavaSettings.getInstance().isDataPlaneClient() && JavaSettings.getInstance().isAzureV1()) {
// special handling for paging method
if (clientMethod.getType().isPaging()) {
String itemName = clientMethod.getMethodPageDetails().getItemName();
// rawResponseType has properties: 'value' and 'nextLink'
IType rawResponseType = clientMethod.getProxyMethod().getRawResponseBodyType();
if (!(rawResponseType instanceof ClassType)) {
throw new IllegalStateException(String.format(
"clientMethod.getProxyMethod().getRawResponseBodyType() should be ClassType for paging method. rawResponseType = %s",
rawResponseType.toString()));
List<ModelPropertySegment> pageItemsPropertyReference
= clientMethod.getMethodPageDetails().getPageItemsPropertyReference();
IType valueListType = pageItemsPropertyReference.get(pageItemsPropertyReference.size() - 1)
.getProperty()
.getClientType();
if (!(valueListType instanceof IterableType)) {
throw new IllegalStateException(
"Page items property must be List or Iterable. ResponseType = " + valueListType);
}
ClientModel model = ClientModelUtil.getClientModel(((ClassType) rawResponseType).getName());
Map<String, ClientModelProperty> properties = new LinkedHashMap<>();
traverseProperties(model, properties);
responseBodyType = properties.values()
.stream()
.filter(property -> property.getName().equals(itemName))
.map(ClientModelProperty::getClientType)
.map(valueListType -> {
// value type is List<T>, we need to get the typeArguments
if (!(valueListType instanceof IterableType)) {
throw new IllegalStateException(
"Type of 'value' property must be List or Iterable, for paging method. ResponseType = "
+ rawResponseType);
}
IType[] listTypeArgs = ((IterableType) valueListType).getTypeArguments();
if (listTypeArgs.length == 0) {
throw new IllegalStateException(
"List or Iterable type does not have template argument. ResponseType = "
+ rawResponseType);
}
return listTypeArgs[0];
})
.findFirst()
.orElse(null);
if (responseBodyType == null) {
throw new IllegalStateException(itemName
+ " not found in properties of rawResponseType. rawResponseType = " + rawResponseType);
IType[] listTypeArgs = ((IterableType) valueListType).getTypeArguments();
if (listTypeArgs.length == 0) {
throw new IllegalStateException(
"Page items List or Iterable does not have a template argument. ResponseType = "
+ valueListType);
}
responseBodyType = listTypeArgs[0];
} else {
responseBodyType = clientMethod.getProxyMethod().getRawResponseBodyType();
}
Expand Down
Loading
Loading