Skip to content

Commit

Permalink
XmsPageableForListCalls - added subscription path to test (#715)
Browse files Browse the repository at this point in the history
* added subscription path to test

* Updated the rule to skip the path if contains resourceGroup

* removed change lo file

* addressed comments and added a check for point get scope

* addressed the comments and also reused the method to check list operations from native rule

* rearranged tests seperately
  • Loading branch information
tejaswiMinnu authored Aug 20, 2024
1 parent f3c16b5 commit 4e5aa05
Show file tree
Hide file tree
Showing 5 changed files with 401 additions and 48 deletions.
150 changes: 149 additions & 1 deletion packages/rulesets/generated/spectral/az-arm.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { oas2, oas3 } from '@stoplight/spectral-formats';
import { pattern, falsy, truthy } from '@stoplight/spectral-functions';
import _, { isEmpty } from 'lodash';
import _, { isEmpty, isNull } from 'lodash';
import { createRulesetFunction } from '@stoplight/spectral-core';
import 'jsonpath-plus';
import 'url';

const avoidAnonymousSchema = (schema, _opts, paths) => {
if (schema === null || schema["x-ms-client-name"] !== undefined) {
Expand Down Expand Up @@ -2981,11 +2983,157 @@ const verifyXMSLongRunningOperationProperty = (pathItem, _opts, paths) => {
return;
};

const parseJsonRef = (ref) => {
return ref.split("#");
};

var Workspace;
(function (Workspace) {
function getSchemaByName(modelName, swaggerPath, inventory) {
var _a;
const root = inventory.getDocuments(swaggerPath);
if (!root || !modelName) {
return undefined;
}
return (_a = root === null || root === void 0 ? void 0 : root.definitions) === null || _a === void 0 ? void 0 : _a[modelName];
}
Workspace.getSchemaByName = getSchemaByName;
function jsonPath(paths, document) {
let root = document;
for (const seg of paths) {
root = root[seg];
if (!root) {
break;
}
}
return root;
}
Workspace.jsonPath = jsonPath;
function resolveRef(schema, inventory) {
function getRef(refValue, swaggerPath) {
const root = inventory.getDocuments(swaggerPath);
if (refValue.startsWith("/")) {
refValue = refValue.substring(1);
}
const segments = refValue.split("/");
return jsonPath(segments, root);
}
let currentSpec = schema.file;
let currentSchema = schema.value;
while (currentSchema && currentSchema.$ref) {
const slices = parseJsonRef(currentSchema.$ref);
currentSpec = slices[0] || currentSpec;
currentSchema = getRef(slices[1], currentSpec);
}
return {
file: currentSpec,
value: currentSchema,
};
}
Workspace.resolveRef = resolveRef;
function getProperty(schema, propertyName, inventory) {
let source = schema;
const visited = new Set();
while (source.value && source.value.$ref && !visited.has(source.value)) {
visited.add(source.value);
source = resolveRef(source, inventory);
}
if (!source || !source.value) {
return undefined;
}
const model = source.value;
if (model.properties && model.properties[propertyName]) {
return resolveRef(createEnhancedSchema(model.properties[propertyName], source.file), inventory);
}
if (model.allOf) {
for (const element of model.allOf) {
const property = getProperty({ file: source.file, value: element }, propertyName, inventory);
if (property) {
return property;
}
}
}
return undefined;
}
Workspace.getProperty = getProperty;
function getProperties(schema, inventory) {
let source = schema;
const visited = new Set();
while (source.value && source.value.$ref && !visited.has(source.value)) {
visited.add(source.value);
source = resolveRef(source, inventory);
}
if (!source || !source.value) {
return [];
}
let result = {};
const model = source.value;
if (model.properties) {
for (const propertyName of Object.keys(model.properties)) {
result[propertyName] = createEnhancedSchema(model.properties[propertyName], source.file);
}
}
if (model.allOf) {
for (const element of model.allOf) {
const properties = getProperties({ file: source.file, value: element }, inventory);
if (properties) {
result = { ...properties, ...result };
}
}
}
return result;
}
Workspace.getProperties = getProperties;
function getAttribute(schema, attributeName, inventory) {
let source = schema;
const visited = new Set();
while (source.value && source.value.$ref && !visited.has(source.value)) {
visited.add(source.value);
source = resolveRef(source, inventory);
}
if (!source) {
return undefined;
}
const attribute = source.value[attributeName];
if (attribute) {
return createEnhancedSchema(attribute, source.file);
}
return undefined;
}
Workspace.getAttribute = getAttribute;
function createEnhancedSchema(schema, specPath) {
return {
file: specPath,
value: schema,
};
}
Workspace.createEnhancedSchema = createEnhancedSchema;
})(Workspace || (Workspace = {}));

require("string.prototype.matchall");
function isListOperation(path) {
if (path.includes(".")) {
const splitNamespace = path.split(".");
if (path.includes("/")) {
const segments = splitNamespace[splitNamespace.length - 1].split("/");
if (segments.length % 2 == 0) {
return true;
}
}
}
return false;
}

const xmsPageableForListCalls = (swaggerObj, _opts, paths) => {
if (swaggerObj === null) {
return [];
}
const path = paths.path || [];
if (!isNull(path[1])) {
if (!isListOperation(path[1].toString())) {
return;
}
}
if (swaggerObj["x-ms-pageable"])
return [];
else
Expand Down
21 changes: 2 additions & 19 deletions packages/rulesets/src/native/utilities/arm-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { ISwaggerInventory, parseJsonRef } from "@microsoft.azure/openapi-validator-core"
import _ from "lodash"
import { nodes } from "./jsonpath"
import { isListOperation } from "./rules-helper"
import { SwaggerHelper } from "./swagger-helper"
import { SwaggerWalker } from "./swagger-walker"
import { Workspace } from "./swagger-workspace"
Expand Down Expand Up @@ -135,24 +136,6 @@ export class ArmHelper {
}
}

private isListOperation(op: Operation) {
const path = op.apiPath
if (path.includes(".")) {
// Get the portion of the api path to the right of the provider namespace by splitting the path by '.' and taking the last element
const splitNamespace = path.split(".")
if (path.includes("/")) {
const segments = splitNamespace[splitNamespace.length - 1].split("/")

// If the last segment split by '/' has even segments, then the operation is a list operation
if (segments.length % 2 == 0) {
return true
}
}
}

return false
}

private getXmsResources() {
for (const name of Object.keys(this.innerDoc.definitions || {})) {
const model = this.getInternalModel(name)
Expand Down Expand Up @@ -315,7 +298,7 @@ export class ArmHelper {
const resWithPutOrPatch = includeGet
? localResourceModels.filter((re) =>
re.operations.some(
(op) => (op.httpMethod === "get" && !this.isListOperation(op)) || op.httpMethod === "put" || op.httpMethod == "patch",
(op) => (op.httpMethod === "get" && !isListOperation(op.apiPath)) || op.httpMethod === "put" || op.httpMethod == "patch",
),
)
: localResourceModels.filter((re) => re.operations.some((op) => op.httpMethod === "put" || op.httpMethod == "patch"))
Expand Down
44 changes: 30 additions & 14 deletions packages/rulesets/src/native/utilities/rules-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,27 +56,26 @@ export function getAllResourceProvidersFromPath(path: string): string[] {
return Array.from(matchAll(path, resourceProviderRegex), (m: any) => m[1])
}

export function getProviderNamespace(apiPath:string) {
export function getProviderNamespace(apiPath: string) {
const matches = getAllResourceProvidersFromPath(apiPath)
if (matches.length) {
return matches.pop()
}
return undefined
}

export function getProviderNamespaceFromPath(filePath:string) {
export function getProviderNamespaceFromPath(filePath: string) {
if (!filePath) {
return undefined
}
const resourceProviderRegex = new RegExp(/\/(Microsoft\.\w+)\//i, "g")
const match = Array.from(matchAll(filePath.replace(/\\/g,"/"),resourceProviderRegex), (m: any) => m[1])
const match = Array.from(matchAll(filePath.replace(/\\/g, "/"), resourceProviderRegex), (m: any) => m[1])
if (match) {
return match[0]
}
return undefined
}


export function getAllWordsFromPath(path: string): string[] {
const wordRegex = new RegExp(/([\w.]+)/, "g")
return Array.from(matchAll(path, wordRegex), (m: any) => m[1])
Expand Down Expand Up @@ -138,14 +137,31 @@ export function stringify(path: string[]) {
return JSONPath.toPathString(pathWithRoot)
}

export function getResourceProvider(inventory:ISwaggerInventory) {
export function isListOperation(path: string) {
if (path.includes(".")) {
// Get the portion of the api path to the right of the provider namespace by splitting the path by '.' and taking the last element
const splitNamespace = path.split(".")
if (path.includes("/")) {
const segments = splitNamespace[splitNamespace.length - 1].split("/")

// If the last segment split by '/' has even segments, then the operation is a list operation
if (segments.length % 2 == 0) {
return true
}
}
}

return false
}

export function getResourceProvider(inventory: ISwaggerInventory) {
const walker = new SwaggerWalker(inventory)
let result: string[] = []
walker.warkAll(["$.paths.*"], (path: string[]) => {
const apiPath = path[2] as string
if (result.length === 0) {
result = [...getAllResourceProvidersFromPath(apiPath)]
}
})
return result.length ? result.pop() || "" : ""
}
let result: string[] = []
walker.warkAll(["$.paths.*"], (path: string[]) => {
const apiPath = path[2] as string
if (result.length === 0) {
result = [...getAllResourceProvidersFromPath(apiPath)]
}
})
return result.length ? result.pop() || "" : ""
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { isNull } from "lodash"
import { isListOperation } from "../../native/utilities/rules-helper"

const xmsPageableForListCalls = (swaggerObj: any, _opts: any, paths: any) => {
if (swaggerObj === null) {
return []
}
const path = paths.path || []

//path array contains 3 values
// 0 - paths
// 1 - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Music/Configurations
// 2 - get
if (!isNull(path[1])) {
if (!isListOperation(path[1].toString())) {
return
}
}
if (swaggerObj["x-ms-pageable"]) return []
else
return [
Expand Down
Loading

0 comments on commit 4e5aa05

Please sign in to comment.