desktop/scripts/swagger/validate-models.ts (230 lines of code) (raw):
// eslint-disable no-console
import "../../src/client/init";
import * as path from "path";
process.env.NODE_PATH = process.env.NODE_PATH + path.delimiter + path.join(__dirname, "../..");
require("module").Module._initPaths();
import "reflect-metadata";
import "zone.js";
console.log("Nodepath", process.env.NODE_PATH);
import { Duration } from "luxon";
import { metadataForCtr } from "../../src/@batch-flask/core/record/helpers";
import * as models from "../../src/app/models";
import { Constants } from "../../src/common";
const dataPlaneVersion = Constants.ApiVersion.batchService;
interface SwaggerProperty {
type: "string" | "integer" | "boolean" | "array" | "number" | undefined;
format: "date-time" | "duration" | "double" | undefined;
$ref: string | undefined;
title: string;
}
interface SwaggerProperties {
[name: string]: SwaggerProperty;
}
interface SwaggerDefinition {
properties?: SwaggerProperties;
enum?: string[];
}
const nameMapping = [
{ swagger: "TaskContainerExecutionInformation", app: "TaskContainerExecutionInfo" },
{ swagger: "TaskFailureInformation", app: "FailureInfo" },
{ swagger: "CloudTask", app: "Task" },
{ swagger: "CloudJob", app: "Job" },
{ swagger: "CloudPool", app: "Pool" },
{ swagger: "ComputeNode", app: "Node" },
{ swagger: "TaskCounts", app: "JobTaskCounts" },
{ swagger: "JobNetworkConfiguration", app: "NetworkConfiguration" },
{ swagger: "ExitConditions", app: "TaskExitConditions" },
{ swagger: "StartTaskInformation", app: "StartTaskInfo" },
{ swagger: "OutputFile", app: "TaskOutputFile" },
{ swagger: "OutputFileDestination", app: "TaskOutputFileDestination" },
{ swagger: "OutputFileBlobContainerDestination", app: "TaskOutputFileContainer" },
{ swagger: "OutputFileUploadOptions", app: "TaskOutputFileUploadOptions" },
];
const swaggerMappings = {};
for (const mapping of nameMapping) {
swaggerMappings[mapping.swagger] = mapping.app;
}
const appMappings = {};
for (const mapping of nameMapping) {
appMappings[mapping.app] = mapping.swagger;
}
async function getSpecs() {
const baseUrl = `https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/batch`;
const url = `${baseUrl}/data-plane/Microsoft.Batch/stable/${dataPlaneVersion}/BatchService.json`;
const response = await fetch(url);
return response.json() as unknown as { definitions: Record<string, any>; };
}
async function getMapping() {
const specs = await getSpecs();
const mappings: any[] = [];
for (const name of Object.keys(specs.definitions)) {
if (name in models) {
mappings.push({
name,
model: models[name],
definition: specs.definitions[name],
});
} else if (name in swaggerMappings && models[swaggerMappings[name]]) {
mappings.push({
name,
model: models[swaggerMappings[name]],
definition: specs.definitions[name],
});
} else {
if (!name.endsWith("Result") && !name.endsWith("Parameter")) {
console.log("\t" + name);
}
}
}
return { mappings, specs: new SwaggerSpecs(specs) };
}
interface ValidationError {
name: string;
message: string;
}
class SwaggerModelValidator {
public errors: ValidationError[] = [];
constructor(
private specs: SwaggerSpecs,
public modelName: string,
private model,
private definition: SwaggerDefinition) {
}
public validate() {
if (this.model.prototype) {
let metadata;
try {
metadata = metadataForCtr(this.model);
} catch (e) {
this.addError("Invalid model. Make sure extends Record and has @Model decorator");
}
if (metadata) {
this.checkMissingProperties(new Set(Object.keys(metadata)), this.definition.properties);
this.checkPropertyTypes(metadata);
}
} else {
// Enum
this.validateEnum();
}
}
private addError(message: string) {
this.errors.push({ name: this.modelName, message });
console.warn(`Error: ${this.modelName} > ${message}`);
}
private checkMissingProperties(properties: Set<string>, swaggerProperties: SwaggerProperties | undefined) {
if (!swaggerProperties) { return; }
for (const name of Object.keys(swaggerProperties)) {
if (!properties.has(name)) {
this.addError(`Missing property ${name}`);
}
}
}
private checkPropertyTypes(metadata) {
for (const name of Object.keys(metadata)) {
const swaggerProperty = this.definition.properties![name];
if (!swaggerProperty) {
// this.addPropertyError(name, `Swagger is missing property`);
continue;
}
const swaggerType = swaggerProperty.type;
const property = metadata[name];
const type = property.type;
if (swaggerType === "string" && swaggerProperty.format === "date-time") {
if (type !== Date) {
this.addPropertyError(name, `Expected type to be a date but was ${type}`);
}
} else if (swaggerType === "string" && swaggerProperty.format === "duration") {
if (type !== Duration) {
this.addPropertyError(name, `Expected type to be a duration but was ${type}`);
}
} else if (swaggerType === "string") {
if (type !== String) {
this.addPropertyError(name, `Expected type to be a string but was ${type}`);
}
} else if (swaggerType === "integer" || swaggerType === "number") {
if (type !== Number) {
this.addPropertyError(name, `Expected type to be a number but was ${type}`);
}
} else if (swaggerType === "boolean") {
if (type !== Boolean) {
this.addPropertyError(name, `Expected type to be a boolean but was ${type}`);
}
} else if (swaggerType === "array") {
if (!property.list) {
this.addPropertyError(name, `Expected type to be a array but wasn't defined as a list. ${property}`
+ `Check it has the @ListProp property not @Prop`);
}
} else if (swaggerProperty.$ref) {
const refTypeName = swaggerProperty.$ref.replace("#/definitions/", "");
const nestedType = this.specs.getDefinition(refTypeName);
if (nestedType.enum) {
if (type !== String) {
this.addPropertyError(name, `Expected type to be a enum ${refTypeName} but was ${type}`);
}
} else {
try {
const modelCls = getModel(refTypeName);
if (modelCls !== property.type) {
this.addPropertyError(name,
`Expected type to be of type ${refTypeName} but was ${property.cls}`);
}
} catch (e) {
this.addPropertyError(name, e.message);
}
}
} else if (swaggerType === "object") {
if (type !== Object) {
this.addPropertyError(name, `Expected type to be an object but was ${type}`);
}
} else {
console.log("Property", this.modelName, name, swaggerProperty, metadata[name]);
process.exit(1);
}
}
}
private addPropertyError(name: string, message: string) {
this.addError(`${name} : ${message}`);
}
private validateEnum() {
const values: string[] = (Object as any).values(this.model);
const swaggerValues = this.definition.enum;
if (!swaggerValues) { return; }
for (const value of swaggerValues) {
if (!values.includes(value)) {
this.addError(`Enum is missing value ${value}. Only has ${values}`);
}
}
}
}
class SwaggerSpecs {
constructor(private specs) { }
public getDefinition(name: string): SwaggerDefinition {
return this.specs.definitions[name];
}
}
function getModel(name: string) {
if (name in models) {
return models[name];
} else if (name in swaggerMappings) {
return models[swaggerMappings[name]];
} else {
throw new Error(`Unknown model ${name}. Need to add an mapping?`);
}
}
async function run() {
console.log("Validating models...");
const { mappings, specs } = await getMapping();
let errors: ValidationError[] = [];
for (const mapping of mappings) {
const validator = new SwaggerModelValidator(specs, mapping.name, mapping.model, mapping.definition);
validator.validate();
errors = errors.concat(validator.errors);
}
if (errors.length > 0) {
console.warn(`\nValidation completed with ${errors.length} errors`);
process.exit(2);
} else {
console.log("All models are valid.");
}
}
run().catch((e) => {
console.error(e);
process.exit(1);
});