[Test] Support readOnly on required properties (#996)

* Add refWithReadOnly
* Update isRefLike() to populate refWithReadOnly
* Add additional test file from mike kistler that should simulate the problem this branch is addressing
* Update test/modelValidation/swaggers/specification/readonlyNotRequired/openapi.json

Co-authored-by: Ke Yu <v-ky@microsoft.com>
Co-authored-by: Scott Beddall (from Dev Box) <scbedd@microsoft.com>
Co-authored-by: Scott Beddall <45376673+scbedd@users.noreply.github.com>
Co-authored-by: Mike Kistler <mikekistler@microsoft.com>
This commit is contained in:
Ke Yu 2023-09-23 03:21:14 +08:00 коммит произвёл GitHub
Родитель 61fe0ba253
Коммит fed7c550d9
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 229 добавлений и 4 удалений

Просмотреть файл

@ -231,6 +231,10 @@ export class JsonLoader implements Loader<Json> {
keepRefSiblings?: boolean keepRefSiblings?: boolean
): Promise<Json> { ): Promise<Json> {
if (isRefLike(object)) { if (isRefLike(object)) {
const refObjResult: any = {};
if (object.readOnly !== undefined) {
refObjResult.refWithReadOnly = object.readOnly;
}
const ref = object.$ref; const ref = object.$ref;
const sp = ref.split("#"); const sp = ref.split("#");
if (sp.length > 2) { if (sp.length > 2) {
@ -248,7 +252,8 @@ export class JsonLoader implements Loader<Json> {
object.$ref = `${mockName}#${refObjPath}`; object.$ref = `${mockName}#${refObjPath}`;
return object; return object;
} }
return { $ref: `${mockName}#${refObjPath}` }; refObjResult.$ref = `${mockName}#${refObjPath}`;
return refObjResult;
} }
const refObj = await this.load( const refObj = await this.load(
pathJoin(pathDirname(relativeFilePath), refFilePath), pathJoin(pathDirname(relativeFilePath), refFilePath),
@ -263,13 +268,15 @@ export class JsonLoader implements Loader<Json> {
object.$ref = `${refMockName}#${refObjPath}`; object.$ref = `${refMockName}#${refObjPath}`;
return object; return object;
} }
return { $ref: `${refMockName}#${refObjPath}` }; refObjResult.$ref = `${refMockName}#${refObjPath}`;
return refObjResult;
} else { } else {
if (keepRefSiblings) { if (keepRefSiblings) {
object.$ref = refMockName; object.$ref = refMockName;
return object; return object;
} }
return { $ref: refMockName }; refObjResult.$ref = refMockName;
return refObjResult;
} }
} }
@ -329,4 +336,5 @@ export class JsonLoader implements Loader<Json> {
} }
} }
export const isRefLike = (obj: any): obj is { $ref: string } => typeof obj.$ref === "string"; export const isRefLike = (obj: any): obj is { $ref: string; readOnly?: boolean } =>
typeof obj.$ref === "string";

Просмотреть файл

@ -247,6 +247,7 @@ export interface Schema extends BaseSchema {
[xmsDiscriminatorValue]?: string; [xmsDiscriminatorValue]?: string;
readOnly?: boolean; readOnly?: boolean;
[xmsMutability]?: Array<"create" | "read" | "update">; [xmsMutability]?: Array<"create" | "read" | "update">;
refWithReadOnly?: boolean;
xml?: XML; xml?: XML;
externalDocs?: ExternalDocs; externalDocs?: ExternalDocs;
example?: { [exampleName: string]: Example }; example?: { [exampleName: string]: Example };

Просмотреть файл

@ -300,6 +300,16 @@ const shouldSkipError = (error: ErrorObject, cxt: SchemaValidateContext) => {
return true; return true;
} }
// If a request is missing a required property that is readOnly we can skip this error
if (
!cxt.isResponse &&
keyword === "required" &&
(parentSchema.properties?.[(params as any).missingProperty]?.refWithReadOnly ||
parentSchema.properties?.[(params as any).missingProperty]?.readOnly)
) {
return true;
}
// If a response has property which x-ms-secret value is "true" in post we can skip this error // If a response has property which x-ms-secret value is "true" in post we can skip this error
if ( if (
cxt.isResponse && cxt.isResponse &&

Просмотреть файл

@ -0,0 +1,28 @@
{
"title": "Create Widget",
"operationId": "Widgets_Create",
"parameters": {
"id": "00000000-0000-0000-0000-000000000000",
"body": {
"description": "Description for sampleWidget",
"secret": "don't tell anyone but your mother",
"topSecret": "don't tell anyone"
}
},
"responses": {
"200": {
"body": {
"widgetId": "00000000-0000-0000-0000-000000000000",
"description": "Description for sampleWidget",
"state": "Active"
}
},
"201": {
"body": {
"widgetId": "00000000-0000-0000-0000-000000000000",
"description": "Description for sampleWidget",
"state": "Active"
}
}
}
}

Просмотреть файл

@ -0,0 +1,171 @@
{
"swagger": "2.0",
"info": {
"title": "Widget Service",
"version": "1.0.0",
"x-typespec-generated": [
{
"emitter": "@azure-tools/typespec-autorest"
}
]
},
"schemes": [
"https"
],
"produces": [
"application/json"
],
"consumes": [
"application/json"
],
"tags": [
{
"name": "Widgets"
}
],
"paths": {
"/widgets/{id}": {
"patch": {
"operationId": "Widgets_Create",
"tags": [
"Widgets"
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/WidgetUpdate"
}
}
],
"responses": {
"200": {
"description": "The request has succeeded.",
"schema": {
"$ref": "#/definitions/Widget"
}
},
"201": {
"description": "The request has succeeded and a new resource has been created as a result.",
"schema": {
"$ref": "#/definitions/Widget"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/Error"
}
}
},
"x-ms-examples": {
"Widgets_Create": {
"$ref": "./CreateWidget.json"
}
}
}
}
},
"definitions": {
"Error": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
]
},
"Widget": {
"type": "object",
"properties": {
"widgetId": {
"type": "string",
"description": "The widget id.",
"readOnly": true
},
"description": {
"type": "string",
"description": "The widget description."
},
"state": {
"$ref": "#/definitions/WidgetState",
"description": "The widget state.",
"readOnly": true
}
},
"required": [
"widgetId",
"state"
]
},
"WidgetState": {
"type": "string",
"description": "The widget state.",
"enum": [
"Active",
"Expired"
],
"x-ms-enum": {
"name": "WidgetState",
"modelAsString": true,
"values": [
{
"name": "Active",
"value": "Active",
"description": "The widget is Active."
},
{
"name": "Expired",
"value": "Expired",
"description": "The widget is Expired."
}
]
}
},
"WidgetUpdate": {
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "The widget description."
},
"secret": {
"type": "string",
"description": "A secret value.",
"x-ms-mutability": [
"update",
"create"
]
},
"topSecret": {
"$ref": "#/definitions/secret",
"description": "A top secret value.",
"x-ms-mutability": [
"update",
"create"
]
}
}
},
"secret": {
"type": "string"
}
},
"parameters": {}
}

Просмотреть файл

@ -894,5 +894,12 @@ describe("Model Validation", () => {
assert.strictEqual(result[0].message, "Expected type object but found type integer"); assert.strictEqual(result[0].message, "Expected type object but found type integer");
assert.strictEqual(result[0].exampleJsonPath, "$responses.200.body.result1['id']"); assert.strictEqual(result[0].exampleJsonPath, "$responses.200.body.result1['id']");
}); });
it("should validate mutable readonly properties without erroring", async() => {
const specPath = `${testPath}/modelValidation/swaggers/specification/readonlyNotRequired/openapi.json`;
const result = await validate.validateExamples(specPath, "Widgets_Create");
assert.strictEqual(result.length, 0);
});
}); });
}); });