powershell/utils/model-state.ts (194 lines of code) (raw):
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Channel, AutorestExtensionHost as Host, JsonPointerSegments as JsonPath, Mapping, RawSourceMap, Message } from '@autorest/extension-base';
import { safeEval, deserialize, Initializer, DeepPartial } from '@azure-tools/codegen';
import { Dictionary } from '@azure-tools/linq';
import { TspHost, TspHostImpl } from './tsp-host';
import { join } from 'path';
export class ModelState<T extends Dictionary<any>> extends Initializer {
public model!: T;
protected documentName!: string;
protected currentPath: JsonPath = new Array<string>();
protected outputFolder!: string;
private context!: any;
private _debug = false;
private _verbose = false;
public constructor(public service: TspHost | Host, objectInitializer?: DeepPartial<ModelState<T>>) {
super();
this.apply(objectInitializer);
}
async init(project?: any) {
if (this.service instanceof TspHostImpl) {
// skip init for tsp
this.outputFolder = await this.getValue('output-folder', './generated');
this.initContext(project);
return this;
}
const m = await ModelState.getModel<T>(this.service);
this.model = m.model;
this.documentName = m.filename;
this.initContext(project);
this._debug = await this.getValue('debug', false);
this._verbose = await this.getValue('verbose', false);
return this;
}
async initContext(project: any) {
this.context = this.context || {
$config: await this.service.getValue(''),
$project: project,
$lib: {
path: require('path')
}
};
return this;
}
async readFile(filename: string): Promise<string> {
return this.service.readFile(filename);
}
async getValue<V>(key: string, defaultValue?: V): Promise<V> {
// check if it's in the model first
let value = this.model && this.model.language && this.model.language.default ? (<any>this.model.language.default)[key] : undefined;
// fall back to the configuration
if (value == null || value === undefined) {
value = await this.service.getValue(key);
}
// try as a safe eval execution.
if (value === null || value === undefined) {
try {
value = safeEval(key, this.context);
}
catch {
value = null;
}
}
if (defaultValue === undefined && value === null) {
throw new Error(`No value for configuration key '${key}' was provided`);
}
if (typeof value === 'string') {
value = await this.resolveVariables(value);
}
// ensure that any content variables are resolved at the end.
return <V>(value !== null ? value : defaultValue);
}
async setValue<V>(key: string, value: V) {
(<any>this.model.language.default)[key] = value;
}
async listInputs(artifactType?: string | undefined): Promise<Array<string>> {
return this.service.listInputs(artifactType);
}
async protectFiles(path: string): Promise<void> {
return this.service.protectFiles(path);
}
writeFile(filename: string, content: string, sourceMap?: undefined, artifactType?: string | undefined): void {
return this.service.writeFile({ filename: this.outputFolder ? join(this.outputFolder, filename) : filename, content: content, sourceMap: sourceMap, artifactType: artifactType });
}
message(message: Message): void {
if (message.Channel === Channel.Debug && this._debug === false) {
return;
}
if (message.Channel === Channel.Verbose && this._verbose === false) {
return;
}
return this.service.message(message);
}
updateConfigurationFile(filename: string, content: string): void {
return this.service.UpdateConfigurationFile(filename, content);
}
async getConfigurationFile(filename: string): Promise<string> {
return this.service.GetConfigurationFile(filename);
}
protected errorCount = 0;
protected static async getModel<T>(service: Host | TspHost) {
const files = await service.listInputs();
const filename = files[0];
if (files.length === 0) {
throw new Error('Inputs missing.');
}
return {
filename,
model: deserialize<T>(await service.readFile(filename), filename)
};
}
cache!: Array<any>;
replacer(key: string, value: any) {
this.cache = this.cache || new Array<any>();
if (typeof value === 'object' && value !== null) {
if (this.cache.indexOf(value) !== -1) {
// Duplicate reference found
try {
// If this value does not reference a parent it can be deduped
return JSON.parse(JSON.stringify(value));
} catch (error) {
// discard key if value cannot be deduped
return;
}
}
// Store value in our collection
this.cache.push(value);
}
return value;
}
async resolveVariables(input: string): Promise<string> {
let output = input;
for (const rx of [/\$\((.*?)\)/g, /\$\{(.*?)\}/g]) {
/* eslint-disable */
for (let match; match = rx.exec(input);) {
const text = match[0];
const inner = match[1];
let value = await this.getValue<any>(inner, null);
if (value !== undefined && value !== null) {
if (typeof value === 'object') {
value = JSON.stringify(value, this.replacer, 2);
}
if (value === '{}') {
value = 'true';
}
output = output.replace(text, value);
}
}
}
return output;
}
public path(...childPath: JsonPath) {
// this strategy for tracking source path locations
// has proved fundementally crappy.
// will be removing this stuff and transitioning to source-track method.
//const result = new ModelState<T>(this.service, <any>this);
//result.currentPath = [...this.currentPath, ...childPath];
// return result;
return this;
}
public checkpoint() {
if (this.errorCount > 0) {
throw new Error();
}
}
protected msg(channel: Channel, message: string, key: Array<string>, details: any) {
this.message({
Channel: channel,
Key: key,
Source: [
{
document: this.documentName,
Position: {
path: this.currentPath
}
}
],
Text: message,
Details: details
});
}
public warning(message: string, key: Array<string>, details?: any) {
this.msg(Channel.Warning, message, key, details);
}
public hint(message: string, key: Array<string>, details?: any) {
this.msg(Channel.Hint, message, key, details);
}
public error(message: string, key: Array<string>, details?: any) {
this.errorCount++;
this.msg(Channel.Error, message, key, details);
}
public fatal(message: string, key: Array<string>, details?: any) {
this.errorCount++;
this.msg(Channel.Fatal, message, key, details);
}
protected output(channel: Channel, message: string, details?: any) {
this.message({
Channel: channel,
Text: message,
Details: details
});
}
public debug(message: string, details: any) {
this.output(Channel.Debug, message, details);
}
public verbose(message: string, details: any) {
this.output(Channel.Verbose, message, details);
}
public log(message: string, details: any) {
this.output(Channel.Information, message, details);
}
}