alwaysai
Version:
The alwaysAI command-line interface (CLI)
184 lines (171 loc) • 6.62 kB
text/typescript
import { CliLeaf, CliTerseError } from '@alwaysai/alwayscli';
import { existsSync } from 'fs';
import { join } from 'path';
import { ModelId } from '../../core/model/model-id';
import { ModelPackageJsonFile } from '../../core/model/model-package-json-file';
import { echo } from '../../util/echo';
import { logger } from '../../util/logger';
import logSymbols = require('log-symbols');
import { ModelJson } from '@alwaysai/model-configuration-schemas';
import { PLEASE_REPORT_THIS_ERROR_MESSAGE } from '../../constants';
import { stringifyError } from '../../util';
export const modelValidatePackage = CliLeaf({
name: 'validate-package',
description:
'Validate integrity of the alwaysai.model.json file in the current directory.',
async action() {
if ((await validatePackageComponent()) === false) {
throw new CliTerseError(
'Failed to validate model package. See above logs for details'
);
}
}
});
export async function validatePackageComponent(dir?: string) {
let success = false;
// If no directory for modelConfig is provided, then check in current directory.
const modelConfigDir = dir ? dir : process.cwd();
echo(`Validating the model package file...`);
if (!ModelPackageJsonFile(modelConfigDir).exists()) {
throw new CliTerseError(
`${logSymbols.error} Could not find the model package file.`
);
}
const modelFile = ModelPackageJsonFile(modelConfigDir);
let modelConfig: ModelJson | undefined = undefined;
try {
modelConfig = modelFile.read();
echo(
`${logSymbols.success} Successfully validated configuration data types.`
);
} catch (err) {
logger.error(`Read model JSON failed: ${stringifyError(err)}`);
const errors = JSON.stringify(modelFile.getErrors(), null, 2);
if (err.name === 'SyntaxError') {
echo(
`${logSymbols.warning} Could not validate the configuration file. Please check the JSON syntax of the configuration file.`
);
} else if (errors !== null) {
echo(
`${logSymbols.warning} Could not validate the configuration. Possible issues with the configuration file fields: \n`
);
// User friendly error handling:
const skipArray = ['if', 'oneOf'];
logger.error(errors);
JSON.parse(errors).forEach((error) => {
if (!skipArray.some((v) => error.keyword.includes(v))) {
switch (error.keyword) {
case 'maxItems':
case 'minItems':
case 'type':
case 'required':
echo(
`${logSymbols.error} ${error.instancePath} ${error.message}`
);
break;
case 'unevaluatedProperties':
echo(
`${logSymbols.error} ${error.instancePath} ${error.message}: ${error.params['unevaluatedProperty']}`
);
break;
case 'enum':
echo(
`${logSymbols.error} ${error.instancePath} ${error.message}: ${error.params['allowedValues']}`
);
break;
case 'additionalProperties':
echo(
`${logSymbols.error} ${error.instancePath} ${error.message}: ${error.params['additionalProperty']}`
);
break;
// Currently, the only field that has pattern validation is the id field, therefore this message is only for that field. IMO ambiguity on which field would have incorrect regex pattern is unneeded, and if in the future we add more regex patterns to the validation, we can change this error message to fit that.
case 'pattern':
echo(
`${logSymbols.error} ${error.instancePath} cannot contain special characters, and must be in the form of publisher/name format.`
);
break;
default:
echo(error);
break;
}
}
});
} else {
throw new CliTerseError(
`Unexpected model validation error occurred! ${PLEASE_REPORT_THIS_ERROR_MESSAGE}\n${err}`
);
}
// Skip further checks if model config doesn't pass initial validation
return false;
}
if (modelConfig === undefined) {
// This should never occur
throw new CliTerseError(
`Model configuration was not loaded! ${PLEASE_REPORT_THIS_ERROR_MESSAGE}`
);
}
const idCheck = await checkIdVariable(modelConfig.id);
const variableCheck = await checkVariableFilepath(modelConfig);
if (idCheck && variableCheck) {
echo(`${logSymbols.success} Validation successful.`);
success = true;
}
return success;
}
async function checkIdVariable(id: string) {
try {
ModelId.parse(id);
echo(`${logSymbols.success} Successfully validated the Model ID.`);
return true;
} catch (err) {
echo(`${logSymbols.warning} ${stringifyError(err)}`);
return false;
}
}
async function checkVariableFilepath(modelJson) {
let success = true;
const cwd = process.cwd();
// The variables in the model json file that have a verifiable filepath.
const filepathVariables = {
config_file: modelJson.model_parameters.config_file,
label_file: modelJson.model_parameters.label_file,
color_file: modelJson.model_parameters.color_file,
model_file: modelJson.model_parameters.model_file
};
for (const key in filepathVariables) {
const value = filepathVariables[key];
// Skip keys not found in the file or set to null
if (value === undefined || value === null) {
continue;
}
if (value.includes('..') || value === '') {
success = false;
echo(
`${logSymbols.warning} "${value}" does not seem to be a valid file path to the ${key}. Please use relative filepath.`
);
continue;
} else if (existsSync(join(cwd, value))) {
// otherwise check if it exists within the current working directory
echo(
`${logSymbols.success} Successfully checked the "${key}" file path.`
);
} else {
// otherwise, check if it exists anywhere in the fs and give warning
success = false;
if (existsSync(value)) {
echo(
`${logSymbols.warning} "${value}" is not in the current working directory. Please make sure all model files are within model directory.`
);
} else {
// otherwise, it doesn't exist or is invalid, so give warning
echo(
`${logSymbols.warning} "${value}" does not seem to be a valid file path to the ${key}.`
);
}
}
}
return success;
}
export const testSuiteFuncs = {
validatePackageComponent
};