[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:
Родитель
61fe0ba253
Коммит
fed7c550d9
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Загрузка…
Ссылка в новой задаче