Skip to content

Commit

Permalink
Merge pull request Azure#577 from chradek/pagination-take-2
Browse files Browse the repository at this point in the history
Pagination support
  • Loading branch information
daviwil authored Mar 4, 2020
2 parents 3e96168 + 510b9d2 commit 8d79206
Show file tree
Hide file tree
Showing 21 changed files with 2,658 additions and 6 deletions.
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@
"unit-test": "mocha -r ts-node/register './test/unit/**/*spec.ts'",
"integration-test": "start-server-and-test start-test-server:v1 http://localhost:3000 generate-and-test",
"integration-test:new": "npm-run-all start-test-server generate-and-test integration-test:alone stop-test-server",
"generate-and-test": "npm-run-all -s build -p generate-bodystring generate-bodycomplex generate-url generate-customurl generate-xmlservice generate-header -s integration-test:alone",
"generate-and-test": "npm-run-all -s build -p generate-bodystring generate-bodycomplex generate-url generate-customurl generate-xmlservice generate-header generate-paging -s integration-test:alone",
"integration-test:alone": "mocha -r ts-node/register './test/integration/**/*spec.ts'",
"start-test-server": "ts-node test/utils/start-server.ts",
"start-test-server:v1": "ts-node test/integration/testserver-v1/index.ts",
"start-test-server:v1": "start-autorest-express node",
"stop-test-server": "stop-autorest-testserver",
"debug": "node --inspect-brk ./dist/src/main.js",
"generate-bodystring": "autorest-beta --add-credentials=false --typescript --output-folder=./test/integration/generated/bodyString --use=. --title=BodyStringClient --input-file=node_modules/@microsoft.azure/autorest.testserver/swagger/body-string.json --package-name=bodyString --package-version=1.0.0-preview1",
"generate-bodycomplex": "autorest-beta --add-credentials=false --typescript --output-folder=./test/integration/generated/bodyComplex --use=. --title=BodyComplexClient --input-file=node_modules/@microsoft.azure/autorest.testserver/swagger/body-complex.json --package-name=bodyString --package-version=1.0.0-preview1",
"generate-url": "autorest-beta --add-credentials=false --typescript --output-folder=./test/integration/generated/url --use=. --title=UrlClient --input-file=node_modules/@microsoft.azure/autorest.testserver/swagger/url.json --package-name=url --package-version=1.0.0-preview1",
"generate-customurl": "autorest-beta --add-credentials=false --typescript --output-folder=./test/integration/generated/customUrl --use=. --title=CustomUrlClient --input-file=node_modules/@microsoft.azure/autorest.testserver/swagger/custom-baseUrl.json --package-name=custom-url --package-version=1.0.0-preview1",
"generate-header": "autorest-beta --add-credentials=false --typescript --output-folder=./test/integration/generated/header --use=. --title=HeaderClient --input-file=node_modules/@microsoft.azure/autorest.testserver/swagger/header.json --package-name=header --package-version=1.0.0-preview1",
"generate-xmlservice": "autorest-beta --add-credentials=false --typescript --output-folder=./test/integration/generated/xmlservice --use=. --title=XmlServiceClient --input-file=node_modules/@microsoft.azure/autorest.testserver/swagger/xml-service.json --package-name=xmlservice --package-version=1.0.0-preview1"
"generate-xmlservice": "autorest-beta --add-credentials=false --typescript --output-folder=./test/integration/generated/xmlservice --use=. --title=XmlServiceClient --input-file=node_modules/@microsoft.azure/autorest.testserver/swagger/xml-service.json --package-name=xmlservice --package-version=1.0.0-preview1",
"generate-paging": "autorest-beta --typescript --add-credentials=false --output-folder=./test/integration/generated/paging --use=. --title=PagingClient --input-file=node_modules/@microsoft.azure/autorest.testserver/swagger/paging.json --package-name=pagingservice --package-version=1.0.0-preview1"
},
"files": [
"dist/**",
Expand All @@ -36,6 +37,7 @@
"@azure-tools/linq": "3.1.206",
"@azure-tools/openapi": "3.0.209",
"@azure/core-http": "^1.0.0",
"@azure/core-paging": "^1.0.0",
"@azure/logger": "^1.0.0",
"@types/lodash": "^4.14.149",
"lodash": "^4.17.15",
Expand Down
36 changes: 36 additions & 0 deletions src/models/operationDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface OperationDetails {
responses: OperationResponseDetails[];
typeDetails: TypeDetails;
mediaTypes: Set<KnownMediaType>;
pagination?: PaginationDetails;
}

/**
Expand Down Expand Up @@ -92,3 +93,38 @@ export interface OperationSpecResponse {
export type OperationSpecResponses = {
[responseCode: string]: OperationResponseMappers;
};

/**
* Operation pagination metadata.
*/
export interface PaginationDetails {
/**
* The name of the field in the response that can be paged over.
*/
itemName: string;
/**
* The possible types for the iterable field.
*/
itemTypes: TypeDetails[];
/**
* Name of the field containing the nextLink value.
* If missing, all results are returned in a single page.
*/
nextLinkName?: string;
/**
* The name of the operation to call with the nextLink.
*/
nextLinkOperationName?: string;
/**
* The name of the operationGroup that nextLinkOperationName resides in.
*/
group?: string;
/**
* The name of the operation that nextLinkOperationName references.
*/
member?: string;
/**
* Indicates whether this operation is used by another operation to get pages.
*/
isNextLinkMethod: boolean;
}
128 changes: 128 additions & 0 deletions src/transforms/extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import {
CodeModel,
Operation,
SchemaType,
Parameter,
StringSchema,
Protocol,
ParameterLocation
} from "@azure-tools/codemodel";
import { cloneOperation } from "../utils/cloneOperation";
import { extractPaginationDetails } from "../utils/extractPaginationDetails";
import { getLanguageMetadata } from "../utils/languageHelpers";

/**
* Normalizes the CodeModel based on available Azure extensions.
* This may result in additional operations being inserted into the model.
* @param codeModel The model that contains all the information required to generate a service API.
*/
export function normalizeModelWithExtensions(codeModel: CodeModel) {
addPageableMethods(codeModel);
}

/**
* Adds the <operationName>Next method for each operation with an x-ms-pageable extension.
* @param codeModel
*/
function addPageableMethods(codeModel: CodeModel) {
const operationGroups = codeModel.operationGroups;

for (const operationGroup of operationGroups) {
const operationGroupMetadata = getLanguageMetadata(operationGroup.language);
const operations = operationGroup.operations.slice();

for (const operation of operations) {
const paginationDetails = extractPaginationDetails(operation);
const operationMetadata = getLanguageMetadata(operation.language);
const operationName = operationMetadata.name;
const operationDescription = operationMetadata.description;

if (!paginationDetails || !paginationDetails.nextLinkName) {
// The operation either doesn't support pagination or returns all items in a single page.
// Therefore, it is not necessary to create a pageable method.
continue;
}

const nextLinkOperationName = paginationDetails.nextLinkOperationName;
if (!nextLinkOperationName) {
// We don't know what the new operation name is.
throw new Error(
`Unable to determine the x-ms-pageable operationName for "${operationName}".`
);
}

// Attempt to find the nextLinkOperationName in the code model.
let nextLinkMethod = findOperation(
codeModel,
paginationDetails.group ?? operationGroupMetadata.name,
nextLinkOperationName
);

if (nextLinkMethod) {
// The operation to call to get subsequent pages already exists, so we don't need to create it.
const metadata = getLanguageMetadata(nextLinkMethod.language);
metadata.paging.isNextLinkMethod = true;
continue;
}

// The "Next" operation doesn't exist, so we need to create it using current operation as a base.
nextLinkMethod = cloneOperation(
operation,
nextLinkOperationName,
operationDescription
);

const nextLinkMethodMetadata = getLanguageMetadata(
nextLinkMethod.language
);
nextLinkMethodMetadata.paging.isNextLinkMethod = true;

// Since this is a brand new operation, the nextLink will be a partial or absolute url.
const nextLinkRequestProtocol =
nextLinkMethod.request.protocol.http ?? new Protocol();
nextLinkRequestProtocol.path = "{nextLink}";

// Create the nextLink parameter.
// This will appear as a required parameter to the "Next" operation.
const httpProtocol = new Protocol();
httpProtocol.in = ParameterLocation.Path;
const nextLinkParameter = new Parameter(
"nextLink",
`The nextLink from the previous successful call to the ${operationName} method.`,
new StringSchema("string", ""),
{
required: true,
language: {
default: {
serializedName: "nextLink"
}
},
extensions: {
"x-ms-skip-url-encoding": true
},
protocol: {
http: httpProtocol
}
}
);
nextLinkMethod.request.addParameter(nextLinkParameter);

operationGroup.addOperation(nextLinkMethod);
}
}
}

function findOperation(
codeModel: CodeModel,
operationGroupName: string,
operationName: string
): Operation | undefined {
const operationGroup = codeModel.getOperationGroup(operationGroupName);
return operationGroup?.operations.find(operation => {
const languageMetadata = getLanguageMetadata(operation.language);
return languageMetadata.name === operationName;
});
}
5 changes: 4 additions & 1 deletion src/transforms/operationTransforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { ParameterDetails } from "../models/parameterDetails";
import { PropertyKind, TypeDetails } from "../models/modelDetails";
import { KnownMediaType } from "@azure-tools/codegen";
import { headersToSchema } from "../utils/headersToSchema";
import { extractPaginationDetails } from "../utils/extractPaginationDetails";

export function transformOperationSpec(
operationDetails: OperationDetails,
Expand Down Expand Up @@ -222,6 +223,7 @@ export async function transformOperation(
operationGroupName: string
): Promise<OperationDetails> {
const metadata = getLanguageMetadata(operation.language);
const pagination = extractPaginationDetails(operation);
const name = normalizeName(metadata.name, NameType.Property);
const operationFullName = `${operationGroupName}_${name}`;
const responsesAndErrors = [
Expand Down Expand Up @@ -252,7 +254,8 @@ export async function transformOperation(
description: metadata.description,
request,
responses,
mediaTypes
mediaTypes,
pagination
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/transforms/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ import { transformObjects, transformObject } from "./objectTransforms";
import { ObjectDetails } from "../models/modelDetails";
import { Host } from "@azure-tools/autorest-extension-base";
import { transformBaseUrl } from "./urlTransforms";
import { KnownMediaType } from "@azure-tools/codegen";
import { OperationGroupDetails } from "../models/operationDetails";
import { normalizeModelWithExtensions } from "./extensions";

export async function transformChoices(codeModel: CodeModel) {
const choices = [
Expand Down Expand Up @@ -57,6 +56,7 @@ export async function transformCodeModel(
host: Host
): Promise<ClientDetails> {
const className = normalizeName(codeModel.info.title, NameType.Class);
normalizeModelWithExtensions(codeModel);

const [uberParents, operationGroups] = await Promise.all([
getUberParents(codeModel),
Expand Down
36 changes: 36 additions & 0 deletions src/utils/cloneOperation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Operation } from "@azure-tools/codemodel";
import { cloneDeep } from "lodash";
import { getLanguageMetadata } from "./languageHelpers";

/**
* Clone an operation and overwrite the operation name and description.
* @param operation
* @param operationName
* @param operationDescription
*/
export function cloneOperation(
operation: Operation,
operationName: string,
operationDescription: string
) {
const operationInitializer = cloneDeep(operation);
// filter out methods
for (const key of Object.keys(operationInitializer)) {
if (typeof (operationInitializer as any)[key] === "function") {
delete (operationInitializer as any)[key];
}
}
const newOperation = new Operation(
operationName,
operationDescription,
operationInitializer
);
const operationMetadata = getLanguageMetadata(newOperation.language);
operationMetadata.name = operationName;
operationMetadata.description = operationName;

return newOperation;
}
Loading

0 comments on commit 8d79206

Please sign in to comment.