Merge pull request #6 from microsoft/codegen_api

feat(modelrepository): provide api for code generation
This commit is contained in:
Eric Chen 2019-11-18 16:04:49 +08:00 коммит произвёл GitHub
Родитель f99279bc0f 4e7c3fb225
Коммит b18fbc5b02
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 1002 добавлений и 544 удалений

16
.vscode/launch.json поставляемый
Просмотреть файл

@ -7,26 +7,20 @@
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
"stopOnEntry": false,
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"sourceMaps": true,
"outFiles": ["${workspaceRoot}/out/src/**/*.js"],
"preLaunchTask": "npm: watch"
"outFiles": ["${workspaceFolder/out/**/*.js"]
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"${workspaceRoot}/test/resources/project1/project.code-workspace",
"--extensionDevelopmentPath=${workspaceRoot}",
"--extensionTestsPath=${workspaceRoot}/out/test"
],
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test"],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": ["${workspaceRoot}/out/test/**/*.js"],
"preLaunchTask": "npm: watch"
"outFiles": ["${workspaceFolder}/out/test/**/*.js"],
"preLaunchTask": "npm: compile"
}
]
}

1231
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -96,21 +96,22 @@
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"tslint": "tslint -t verbose src/**/*.ts",
"test": "node ./out/test/runTest.js"
},
"devDependencies": {
"@types/fs-extra": "^7.0.0",
"@types/glob": "^7.1.1",
"@types/keytar": "^4.4.0",
"@types/mocha": "^5.2.6",
"@types/node": "^10.12.21",
"@types/vscode": "^1.36.0",
"@types/request-promise": "^4.1.44",
"@types/keytar": "^4.4.0",
"@types/vscode": "^1.36.0",
"cz-conventional-changelog": "^3.0.2",
"glob": "^7.1.4",
"mocha": "^6.1.4",
"prettier": "^1.19.1",
"prettier-tslint": "^0.4.2",
"tslint": "^5.12.1",
"typescript": "^3.3.1",
"vscode-test": "^1.0.0-next.0"

30
src/api/apiProvider.ts Normal file
Просмотреть файл

@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { ModelType } from "../deviceModel/deviceModelManager";
import { ModelRepositoryManager } from "../modelRepository/modelRepositoryManager";
import { UI } from "../view/ui";
import { UIConstants } from "../view/uiConstants";
/**
* Api provider for extension integration
*/
export class ApiProvider {
constructor(private readonly modelRepositoryManager: ModelRepositoryManager) {}
/**
* select capability model
*/
public async selectCapabilityModel(): Promise<string> {
return await UI.selectOneModelFile(UIConstants.SELECT_CAPABILITY_MODEL_LABEL, ModelType.CapabilityModel);
}
/**
* download dependent interface of capability model
* @param folder folder to download interface
* @param capabilityModelFile capability model file path
*/
public async downloadDependentInterface(folder: string, capabilityModelFile: string): Promise<void> {
await this.modelRepositoryManager.downloadDependentInterface(folder, capabilityModelFile);
}
}

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

@ -41,6 +41,7 @@ export class Constants {
public static readonly PUBLIC_REPOSITORY_URL_NOT_FOUND_MSG = "Public repository url is not found";
public static readonly CONNECTION_STRING_INVALID_FORMAT_MSG = "Invalid connection string format";
public static readonly MODEL_TYPE_INVALID_MSG = "Invalid model type";
public static readonly NEED_OPEN_COMPANY_REPOSITORY_MSG = "Please open company repository and try again";
public static readonly NSAT_SURVEY_URL = "https://aka.ms/vscode-azure-digital-twins-survey";
public static readonly WEB_VIEW_PATH = "assets/modelRepository";

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

@ -121,7 +121,7 @@ export class Utility {
* @param filePath file path
*/
public static async getModelFileInfo(filePath: string): Promise<ModelFileInfo | undefined> {
const content = await fs.readJson(filePath, { encoding: Constants.UTF8 });
const content = await Utility.getJsonContent(filePath);
const modelId: string = content[DigitalTwinConstants.ID];
const context: string = content[DigitalTwinConstants.CONTEXT];
const modelType: ModelType = DeviceModelManager.convertToModelType(content[DigitalTwinConstants.TYPE]);

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

@ -7,8 +7,8 @@ import { ColorizedChannel } from "../common/colorizedChannel";
import { Constants } from "../common/constants";
import { ProcessError } from "../common/processError";
import { Utility } from "../common/utility";
import { MessageType, UI } from "../views/ui";
import { UIConstants } from "../views/uiConstants";
import { MessageType, UI } from "../view/ui";
import { UIConstants } from "../view/uiConstants";
/**
* DigitalTwin model type

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

@ -2,6 +2,7 @@
// Licensed under the MIT license.
import * as vscode from "vscode";
import { ApiProvider } from "./api/apiProvider";
import { ColorizedChannel } from "./common/colorizedChannel";
import { Command } from "./common/command";
import { Constants } from "./common/constants";
@ -16,8 +17,7 @@ import { DigitalTwinHoverProvider } from "./intelliSense/digitalTwinHoverProvide
import { IntelliSenseUtility } from "./intelliSense/intelliSenseUtility";
import { SearchResult } from "./modelRepository/modelRepositoryInterface";
import { ModelRepositoryManager } from "./modelRepository/modelRepositoryManager";
import { MessageType, UI } from "./views/ui";
import { UIConstants } from "./views/uiConstants";
import { MessageType, UI } from "./view/ui";
export function activate(context: vscode.ExtensionContext) {
const outputChannel = new ColorizedChannel(Constants.CHANNEL_NAME);
@ -25,6 +25,7 @@ export function activate(context: vscode.ExtensionContext) {
const nsat = new NSAT(Constants.NSAT_SURVEY_URL, telemetryClient);
const deviceModelManager = new DeviceModelManager(context, outputChannel);
const modelRepositoryManager = new ModelRepositoryManager(context, outputChannel, Constants.WEB_VIEW_PATH);
const apiProvider = new ApiProvider(modelRepositoryManager);
telemetryClient.sendEvent(Constants.EXTENSION_ACTIVATED_MSG);
context.subscriptions.push(outputChannel);
@ -153,6 +154,8 @@ export function activate(context: vscode.ExtensionContext) {
);
},
);
// provide api integration
return { apiProvider };
}
export function deactivate() {}

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

@ -130,7 +130,7 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
includeValue: boolean,
separator: string,
): vscode.CompletionItem[] {
const result: vscode.CompletionItem[] = [];
const completionItems: vscode.CompletionItem[] = [];
const exist = new Set<string>();
const classNode: ClassNode | undefined = DigitalTwinCompletionItemProvider.getObjectType(node, exist);
if (!classNode) {
@ -140,7 +140,7 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
if (!exist.has(DigitalTwinConstants.TYPE)) {
// suggest @type property
const dummyNode: PropertyNode = { id: DigitalTwinConstants.TYPE };
result.push(
completionItems.push(
DigitalTwinCompletionItemProvider.createCompletionItem(
`${dummyNode.id} ${DigitalTwinConstants.REQUIRED_PROPERTY_LABEL}`,
true,
@ -159,7 +159,7 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
if (!child.label || exist.has(child.label)) {
continue;
}
result.push(
completionItems.push(
DigitalTwinCompletionItemProvider.createCompletionItem(
DigitalTwinCompletionItemProvider.formatLabel(child.label, required),
true,
@ -177,9 +177,9 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
exist,
required,
);
result.push(...suggestion);
completionItems.push(...suggestion);
}
return result;
return completionItems;
}
/**
@ -275,7 +275,7 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
exist: Set<string>,
required: Set<string>,
): vscode.CompletionItem[] {
const result: vscode.CompletionItem[] = [];
const completionItems: vscode.CompletionItem[] = [];
const properties: PropertyNode[] = [];
const propertyNode: PropertyNode | undefined = IntelliSenseUtility.getPropertyNode(DigitalTwinConstants.ID);
if (propertyNode) {
@ -289,7 +289,7 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
if (exist.has(property.id)) {
continue;
}
result.push(
completionItems.push(
DigitalTwinCompletionItemProvider.createCompletionItem(
DigitalTwinCompletionItemProvider.formatLabel(property.id, required),
true,
@ -299,7 +299,7 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
),
);
}
return result;
return completionItems;
}
/**
@ -356,16 +356,16 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
range: vscode.Range,
separator: string,
): vscode.CompletionItem[] {
const result: vscode.CompletionItem[] = [];
const completionItems: vscode.CompletionItem[] = [];
const propertyPair: PropertyPair | undefined = IntelliSenseUtility.parseProperty(node);
if (!propertyPair) {
return result;
return completionItems;
}
let propertyNode: PropertyNode | undefined;
let propertyName: string = propertyPair.name.value as string;
if (propertyName === DigitalTwinConstants.CONTEXT) {
// suggest value of @context property
result.push(
completionItems.push(
DigitalTwinCompletionItemProvider.createCompletionItem(
DigitalTwinConstants.IOT_MODEL_LABEL,
false,
@ -384,7 +384,7 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
const classes: ClassNode[] = IntelliSenseUtility.getObjectClasses(propertyNode);
for (const classNode of classes) {
const value: string = DigitalTwinGraph.getClassType(classNode);
result.push(
completionItems.push(
DigitalTwinCompletionItemProvider.createCompletionItem(
value,
false,
@ -403,7 +403,7 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
if (propertyNode) {
const enums = IntelliSenseUtility.getEnums(propertyNode);
for (const value of enums) {
result.push(
completionItems.push(
DigitalTwinCompletionItemProvider.createCompletionItem(
value,
false,
@ -415,7 +415,7 @@ export class DigitalTwinCompletionItemProvider implements vscode.CompletionItemP
}
}
}
return result;
return completionItems;
}
/**

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

@ -13,8 +13,8 @@ import { UserCancelledError } from "../common/userCancelledError";
import { Utility } from "../common/utility";
import { ModelType } from "../deviceModel/deviceModelManager";
import { DigitalTwinConstants } from "../intelliSense/digitalTwinConstants";
import { ChoiceType, MessageType, UI } from "../views/ui";
import { UIConstants } from "../views/uiConstants";
import { ChoiceType, MessageType, UI } from "../view/ui";
import { UIConstants } from "../view/uiConstants";
import { ModelRepositoryClient } from "./modelRepositoryClient";
import { ModelRepositoryConnection } from "./modelRepositoryConnection";
import { GetResult, SearchResult } from "./modelRepositoryInterface";
@ -78,16 +78,23 @@ export class ModelRepositoryManager {
if (!connectionString) {
throw new Error(Constants.CONNECTION_STRING_NOT_FOUND_MSG);
}
const connection: ModelRepositoryConnection = ModelRepositoryConnection.parse(connectionString);
return {
hostname: Utility.enforceHttps(connection.hostName),
apiVersion: Constants.MODEL_REPOSITORY_API_VERSION,
repositoryId: connection.repositoryId,
accessToken: connection.generateAccessToken(),
};
return ModelRepositoryManager.getCompanyRepositoryInfo(connectionString);
}
}
/**
* get available repository info, company repository is prior to public repository
*/
private static async getAvailableRepositoryInfo(): Promise<RepositoryInfo[]> {
const repoInfos: RepositoryInfo[] = [];
const connectionString: string | null = await CredentialStore.get(Constants.MODEL_REPOSITORY_CONNECTION_KEY);
if (connectionString) {
repoInfos.push(ModelRepositoryManager.getCompanyRepositoryInfo(connectionString));
}
repoInfos.push(await ModelRepositoryManager.createRepositoryInfo(true));
return repoInfos;
}
/**
* set up company model repository connection
*/
@ -98,13 +105,7 @@ export class ModelRepositoryManager {
connectionString = await UI.inputConnectionString(UIConstants.INPUT_REPOSITORY_CONNECTION_STRING_LABEL);
newConnection = true;
}
const connection: ModelRepositoryConnection = ModelRepositoryConnection.parse(connectionString);
const repoInfo: RepositoryInfo = {
hostname: Utility.enforceHttps(connection.hostName),
apiVersion: Constants.MODEL_REPOSITORY_API_VERSION,
repositoryId: connection.repositoryId,
accessToken: connection.generateAccessToken(),
};
const repoInfo: RepositoryInfo = ModelRepositoryManager.getCompanyRepositoryInfo(connectionString);
// test connection by calling searchModel
await ModelRepositoryClient.searchModel(repoInfo, ModelType.Interface, Constants.EMPTY_STRING, 1, null);
if (newConnection) {
@ -112,6 +113,20 @@ export class ModelRepositoryManager {
}
}
/**
* get company repository info
* @param connectionString connection string
*/
private static getCompanyRepositoryInfo(connectionString: string): RepositoryInfo {
const connection: ModelRepositoryConnection = ModelRepositoryConnection.parse(connectionString);
return {
hostname: Utility.enforceHttps(connection.hostName),
apiVersion: Constants.MODEL_REPOSITORY_API_VERSION,
repositoryId: connection.repositoryId,
accessToken: connection.generateAccessToken(),
};
}
/**
* validate model id list
* @param modelIds model id list
@ -258,7 +273,6 @@ export class ModelRepositoryManager {
if (publicRepository) {
throw new BadRequestError(`${RepositoryType.Public} not support delete operation`);
}
ModelRepositoryManager.validateModelIds(modelIds);
try {
@ -290,16 +304,42 @@ export class ModelRepositoryManager {
}
/**
* download denpendent interface models from capability model
* @param folder folder to download models
* @param filePath capability model file path
* download dependent interface of capability model, throw exception when interface not found
* @param folder folder to download interface
* @param capabilityModelFile capability model file path
*/
public async downloadDependentInterface(folder: string, filePath: string): Promise<void> {
// TODO:(erichen): for code gen integration, used as api
// get existing interface files
// find dependent interface file list from dcm file
// diff the files to download
// download interface files
public async downloadDependentInterface(folder: string, capabilityModelFile: string): Promise<void> {
if (!folder || !capabilityModelFile) {
throw new BadRequestError(`folder and capabilityModelFile ${Constants.NOT_EMPTY_MSG}`);
}
// get implemented interface of capability model
const content = await Utility.getJsonContent(capabilityModelFile);
const implementedInterface = content[DigitalTwinConstants.IMPLEMENTS];
if (!implementedInterface || implementedInterface.length === 0) {
throw new BadRequestError("no implemented interface found in capability model");
}
// get existing interface file in workspace
const repoInfos: RepositoryInfo[] = await ModelRepositoryManager.getAvailableRepositoryInfo();
const fileInfos: ModelFileInfo[] = await UI.findModelFiles(ModelType.Interface);
const exist = new Set<string>(fileInfos.map((f) => f.id));
let schema: any;
let found: boolean;
let message: string;
for (const item of implementedInterface) {
schema = item[DigitalTwinConstants.SCHEMA];
if (typeof schema !== "string" || exist.has(schema)) {
continue;
}
found = await this.doDownloadModel(repoInfos, schema, folder);
if (!found) {
message = `interface ${schema} not found`;
if (repoInfos.length === 1) {
message = `${message}. ${Constants.NEED_OPEN_COMPANY_REPOSITORY_MSG}`;
}
throw new BadRequestError(message);
}
}
}
/**
@ -323,12 +363,12 @@ export class ModelRepositoryManager {
}
/**
* download model from repository
* download model from repository, return false if model not found
* @param repoInfos repository info list
* @param modelId model id
* @param folder folder to download model
*/
private async doDownloadModel(repoInfos: RepositoryInfo[], modelId: string, folder: string): Promise<void> {
private async doDownloadModel(repoInfos: RepositoryInfo[], modelId: string, folder: string): Promise<boolean> {
let result: GetResult | undefined;
for (const repoInfo of repoInfos) {
try {
@ -346,7 +386,9 @@ export class ModelRepositoryManager {
}
if (result) {
await Utility.createModelFile(folder, result.modelId, result.content);
return true;
}
return false;
}
/**

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

@ -3,6 +3,7 @@
import * as path from "path";
import * as vscode from "vscode";
import { Constants } from "../common/constants";
import { UserCancelledError } from "../common/userCancelledError";
import { Utility } from "../common/utility";
import { ModelType } from "../deviceModel/deviceModelManager";
@ -90,7 +91,7 @@ export class UI {
};
});
}
items.push({ label: UIConstants.BROWSE_LABEL, description: "" });
items.push({ label: UIConstants.BROWSE_LABEL, description: Constants.EMPTY_STRING });
const selected: vscode.QuickPickItem = await UI.showQuickPick(label, items);
return selected.description || (await UI.showOpenDialog(label));
}
@ -105,11 +106,11 @@ export class UI {
placeHolder: label,
ignoreFocusOut: true,
};
const result: vscode.QuickPickItem | undefined = await vscode.window.showQuickPick(items, options);
if (!result) {
const selected: vscode.QuickPickItem | undefined = await vscode.window.showQuickPick(items, options);
if (!selected) {
throw new UserCancelledError(label);
}
return result;
return selected;
}
/**
@ -125,11 +126,11 @@ export class UI {
canSelectFolders: true,
canSelectMany: false,
};
const results: vscode.Uri[] | undefined = await vscode.window.showOpenDialog(options);
if (!results || results.length === 0) {
const selected: vscode.Uri[] | undefined = await vscode.window.showOpenDialog(options);
if (!selected || selected.length === 0) {
throw new UserCancelledError(label);
}
return results[0].fsPath;
return selected[0].fsPath;
}
/**
@ -168,11 +169,11 @@ export class UI {
value,
ignoreFocusOut,
};
const result: string | undefined = await vscode.window.showInputBox(options);
if (!result) {
const input: string | undefined = await vscode.window.showInputBox(options);
if (!input) {
throw new UserCancelledError(label);
}
return result;
return input;
}
/**
@ -187,42 +188,23 @@ export class UI {
}
/**
* select model files
* select model files by type
* @param label label
* @param type model type
*/
public static async selectModelFiles(label: string, type?: ModelType): Promise<string[] | undefined> {
const files: vscode.Uri[] = await vscode.workspace.findFiles(UIConstants.MODEL_FILE_GLOB);
if (files.length === 0) {
UI.showNotification(MessageType.Warn, UIConstants.MODELS_NOT_FOUND_MSG);
return undefined;
}
// process in parallel
const items: Array<QuickPickItemWithData<string>> = [];
await Promise.all(
files.map(async (f) => {
let fileInfo: ModelFileInfo | undefined;
try {
fileInfo = await Utility.getModelFileInfo(f.fsPath);
} catch {
// skip if file is not a valid json
return;
}
if (fileInfo) {
if (!type || type === fileInfo.type) {
items.push({
label: path.basename(fileInfo.filePath),
description: fileInfo.id,
data: fileInfo.filePath,
});
}
}
}),
);
if (items.length === 0) {
const fileInfos: ModelFileInfo[] = await UI.findModelFiles(type);
if (fileInfos.length === 0) {
UI.showNotification(MessageType.Warn, UIConstants.MODELS_NOT_FOUND_MSG);
return undefined;
}
const items: Array<QuickPickItemWithData<string>> = fileInfos.map((f) => {
return {
label: path.basename(f.filePath),
description: f.id,
data: f.filePath,
};
});
const selected: Array<QuickPickItemWithData<string>> | undefined = await vscode.window.showQuickPick(items, {
placeHolder: label,
ignoreFocusOut: true,
@ -235,6 +217,67 @@ export class UI {
return selected.map((s) => s.data);
}
/**
* select one model file
* @param label label
* @param type model type
*/
public static async selectOneModelFile(label: string, type?: ModelType): Promise<string> {
const fileInfos: ModelFileInfo[] = await UI.findModelFiles(type);
if (fileInfos.length === 0) {
UI.showNotification(MessageType.Warn, UIConstants.MODELS_NOT_FOUND_MSG);
return Constants.EMPTY_STRING;
}
const items: Array<QuickPickItemWithData<string>> = fileInfos.map((f) => {
return {
label: path.basename(f.filePath),
description: f.id,
data: f.filePath,
};
});
const selected: QuickPickItemWithData<string> | undefined = await vscode.window.showQuickPick(items, {
placeHolder: label,
ignoreFocusOut: true,
canPickMany: false,
matchOnDescription: true,
});
if (!selected) {
throw new UserCancelledError(label);
}
return selected.data;
}
/**
* find model files by type
* @param type model type
*/
public static async findModelFiles(type?: ModelType): Promise<ModelFileInfo[]> {
const fileInfos: ModelFileInfo[] = [];
const files: vscode.Uri[] = await vscode.workspace.findFiles(UIConstants.MODEL_FILE_GLOB);
if (files.length === 0) {
return fileInfos;
}
// process in parallel
await Promise.all(
files.map(async (f) => {
let fileInfo: ModelFileInfo | undefined;
try {
fileInfo = await Utility.getModelFileInfo(f.fsPath);
} catch {
// skip if file is not a valid json
return;
}
if (!fileInfo) {
return;
}
if (!type || type === fileInfo.type) {
fileInfos.push(fileInfo);
}
}),
);
return fileInfos;
}
/**
* ensure files saved
* @param label label

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

@ -10,6 +10,7 @@ export class UIConstants {
public static readonly BROWSE_LABEL = "Browse...";
public static readonly SELECT_REPOSITORY_LABEL = "Select model repository";
public static readonly SELECT_MODELS_LABEL = "Select device models";
public static readonly SELECT_CAPABILITY_MODEL_LABEL = "Select a capability model";
public static readonly INPUT_REPOSITORY_CONNECTION_STRING_LABEL = "Input company repository connection string";
public static readonly SAVE_FILE_CHANGE_LABEL = "Save file change";
public static readonly MODEL_REPOSITORY_TITLE = "IoT Plug and Play Model Repository";