Skip to content

Commit

Permalink
Merge pull request #2232 from codefori/feature/extendable_components
Browse files Browse the repository at this point in the history
Extend the base API to allow other extensions to add their own components.
  • Loading branch information
worksofliam authored Oct 7, 2024
2 parents afc71a2 + f79e370 commit 2d65d6a
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 250 deletions.
13 changes: 7 additions & 6 deletions src/api/IBMi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import * as node_ssh from "node-ssh";
import os from "os";
import path, { parse as parsePath } from 'path';
import * as vscode from "vscode";
import { ComponentId, ComponentManager } from "../components/component";
import { IBMiComponent, IBMiComponentType } from "../components/component";
import { CopyToImport } from "../components/copyToImport";
import { ComponentManager } from "../components/manager";
import { instance } from "../instantiate";
import { CommandData, CommandResult, ConnectionData, IBMiMember, RemoteCommand, SpecialAuthorities, WrapResult } from "../typings";
import { CompileTools } from "./CompileTools";
Expand Down Expand Up @@ -54,7 +55,7 @@ export default class IBMi {
/** User default CCSID is job default CCSID */
private userDefaultCCSID: number = 0;

private components: ComponentManager = new ComponentManager();
private componentManager = new ComponentManager(this);

client: node_ssh.NodeSSH;
currentHost: string = ``;
Expand Down Expand Up @@ -930,7 +931,7 @@ export default class IBMi {
}

progress.report({ message: `Checking Code for IBM i components.` });
await this.components.startup(this);
await this.componentManager.startup();

if (!reconnecting) {
vscode.workspace.getConfiguration().update(`workbench.editor.enablePreview`, false, true);
Expand Down Expand Up @@ -1352,8 +1353,8 @@ export default class IBMi {
}
}

getComponent<T>(id: ComponentId) {
return this.components.get<T>(id);
getComponent<T extends IBMiComponent>(type: IBMiComponentType<T>, ignoreState?: boolean): T | undefined {
return this.componentManager.get<T>(type, ignoreState);
}

/**
Expand Down Expand Up @@ -1383,7 +1384,7 @@ export default class IBMi {

if (lastStmt) {
if ((asUpper?.startsWith(`SELECT`) || asUpper?.startsWith(`WITH`))) {
const copyToImport = this.getComponent<CopyToImport>(`CopyToImport`);
const copyToImport = this.getComponent<CopyToImport>(CopyToImport);
if (copyToImport) {
returningAsCsv = copyToImport.wrap(lastStmt);
list.push(...returningAsCsv.newStatements);
Expand Down
116 changes: 71 additions & 45 deletions src/components/component.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,87 @@
import IBMi from "../api/IBMi";
import { CopyToImport } from "./copyToImport";
import { GetMemberInfo } from "./getMemberInfo";
import { GetNewLibl } from "./getNewLibl";

export enum ComponentState {
NotChecked = `NotChecked`,
NotInstalled = `NotInstalled`,
Installed = `Installed`,
Error = `Error`,
}
interface ComponentRegistry {
GetNewLibl?: GetNewLibl;
CopyToImport?: CopyToImport;
GetMemberInfo?: GetMemberInfo;
}

export type ComponentId = keyof ComponentRegistry;

export abstract class ComponentT {
public state: ComponentState = ComponentState.NotChecked;
public currentVersion: number = 0;
export type ComponentState = `NotChecked` | `NotInstalled` | `Installed` | `NeedsUpdate` | `Error`;

constructor(public connection: IBMi) { }

abstract getInstalledVersion(): Promise<number | undefined>;
abstract checkState(): Promise<boolean>
abstract getState(): ComponentState;
export type ComponentIdentification = {
name: string
version: number
}

export class ComponentManager {
private registered: ComponentRegistry = {};
export type IBMiComponentType<T extends IBMiComponent> = new (c: IBMi) => T;

public async startup(connection: IBMi) {
this.registered.GetNewLibl = new GetNewLibl(connection);
await ComponentManager.checkState(this.registered.GetNewLibl);
/**
* Defines a component that is managed per IBM i.
*
* Any class extending {@link IBMiComponent} needs to register itself in the Component Registry.
*
* For example, this class:
* ```
* class MyIBMIComponent extends IBMiComponent {
* //implements getName(), getRemoteState() and update()
* }
* ```
* Must be registered like this, when the extension providing the component gets activated:
* ```
* export async function activate(context: ExtensionContext) {
* const codeForIBMiExtension = vscode.extensions.getExtension<CodeForIBMi>('halcyontechltd.code-for-ibmi');
* if (codeForIBMiExtension) {
* codeForIBMi = codeForIBMiExtension.isActive ? codeForIBMiExtension.exports : await codeForIBMiExtension.activate();
* codeForIBMi.componentRegistry.registerComponent(context, MyIBMIComponent);
* }
* }
* ```
*
*/
export abstract class IBMiComponent {
private state: ComponentState = `NotChecked`;

this.registered.CopyToImport = new CopyToImport(connection);
await ComponentManager.checkState(this.registered.CopyToImport);
constructor(protected readonly connection: IBMi) {

this.registered.GetMemberInfo = new GetMemberInfo(connection);
await ComponentManager.checkState(this.registered.GetMemberInfo);
}

// TODO: return type based on ComponentIds
get<T>(id: ComponentId): T | undefined {
const component = this.registered[id];
if (component && component.getState() === ComponentState.Installed) {
return component as T;
}
getState() {
return this.state;
}

private static async checkState(component: ComponentT) {
async check() {
try {
await component.checkState();
} catch (e) {
console.log(component);
console.log(`Error checking state for ${component.constructor.name}`, e);
this.state = await this.getRemoteState();
if (this.state !== `Installed`) {
this.state = await this.update();
}
}
catch (error) {
console.log(`Error occurred while checking component ${this.toString()}`);
console.log(error);
this.state = `Error`;
}

return this;
}

toString() {
const identification = this.getIdentification();
return `${identification.name} (version ${identification.version})`
}

/**
* The name of this component; mainly used for display and logging purposes
*
* @returns a human-readable name
*/
abstract getIdentification(): ComponentIdentification;

/**
* @returns the component's {@link ComponentState state} on the IBM i
*/
protected abstract getRemoteState(): ComponentState | Promise<ComponentState>;

/**
* Called whenever the components needs to be installed or updated, depending on its {@link ComponentState state}.
*
* The Component Manager is responsible for calling this, so the {@link ComponentState state} doesn't need to be checked here.
*
* @returns the component's {@link ComponentState state} after the update is done
*/
protected abstract update(): ComponentState | Promise<ComponentState>
}
35 changes: 14 additions & 21 deletions src/components/copyToImport.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,8 @@
import IBMi from "../api/IBMi";
import { Tools } from "../api/Tools";
import { WrapResult } from "../typings";
import { ComponentState, ComponentT } from "./component";

export class CopyToImport implements ComponentT {
private readonly name = 'CPYTOIMPF';
public state: ComponentState = ComponentState.Installed;
public currentVersion: number = 1;

constructor(public connection: IBMi) { }

async getInstalledVersion(): Promise<number> {
return 1;
}

async checkState(): Promise<boolean> {
return true;
}

getState(): ComponentState {
return this.state;
}
import { ComponentState, IBMiComponent } from "./component";

export class CopyToImport extends IBMiComponent {
static isSimple(statement: string): boolean {
statement = statement.trim();
if (statement.endsWith(';')) {
Expand All @@ -32,6 +13,18 @@ export class CopyToImport implements ComponentT {
return parts.length === 4 && parts[0].toUpperCase() === `SELECT` && parts[1] === `*` && parts[2].toUpperCase() === `FROM` && parts[3].includes(`.`);
}

getIdentification() {
return { name: 'CopyToImport', version: 1 };
}

protected getRemoteState(): ComponentState {
return `Installed`;
}

protected update(): ComponentState | Promise<ComponentState> {
return this.getRemoteState();
}

wrap(statement: string): WrapResult {
const outStmf = this.connection.getTempRemote(Tools.makeid())!;

Expand Down
Loading

0 comments on commit 2d65d6a

Please sign in to comment.