Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 0 additions & 53 deletions src/api/ccs/sourceControl.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import * as path from "path";
import * as vscode from "vscode";

import { AtelierAPI } from "../../api";
import { SourceControlApi } from "../../api/ccs/sourceControl";
import { ContextExpressionClient } from "../sourcecontrol/clients/contextExpressionClient";
import { handleError } from "../../utils";

interface ResolveContextExpressionResponse {
status?: string;
textExpression?: string;
message?: string;
}
const sharedClient = new ContextExpressionClient();

export async function resolveContextExpression(): Promise<void> {
const editor = vscode.window.activeTextEditor;
Expand All @@ -28,23 +23,11 @@ export async function resolveContextExpression(): Promise<void> {
}

const routine = path.basename(document.fileName);
const api = new AtelierAPI(document.uri);

let sourceControlApi: SourceControlApi;
try {
sourceControlApi = SourceControlApi.fromAtelierApi(api);
} catch (error) {
void vscode.window.showErrorMessage(error instanceof Error ? error.message : String(error));
return;
}

try {
const response = await sourceControlApi.post<ResolveContextExpressionResponse>("/resolveContextExpression", {
routine,
contextExpression,
});
const response = await sharedClient.resolve(document, { routine, contextExpression });
const data = response ?? {};

const data = response.data ?? {};
if (typeof data.status === "string" && data.status.toLowerCase() === "success" && data.textExpression) {
const eol = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n";
const textExpression = data.textExpression.replace(/\r?\n/g, eol);
Expand Down
13 changes: 13 additions & 0 deletions src/ccs/config/schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Configuração do módulo CCS

As opções abaixo ficam no escopo `objectscript.ccs` e controlam as integrações específicas
para o fork da Consistem.

| Chave | Tipo | Padrão | Descrição |
| ---------------- | ------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------- |
| `endpoint` | `string` | `undefined` | URL base alternativa para a API. Se não definida, a URL é derivada da conexão ativa do Atelier. |
| `requestTimeout` | `number` | `500` | Tempo limite (ms) aplicado às chamadas HTTP do módulo. Valores menores ou inválidos são normalizados para zero. |
| `debugLogging` | `boolean` | `false` | Quando verdadeiro, registra mensagens detalhadas no `ObjectScript` Output Channel. |
| `flags` | `Record<string, boolean>` | `{}` | Feature flags opcionais que podem ser lidas pelas features do módulo. |

Essas configurações não exigem reload da janela; toda leitura é feita sob demanda.
51 changes: 51 additions & 0 deletions src/ccs/config/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as vscode from "vscode";

export interface CcsSettings {
endpoint?: string;
requestTimeout: number;
debugLogging: boolean;
flags: Record<string, boolean>;
}

const CCS_CONFIGURATION_SECTION = "objectscript.ccs";
const DEFAULT_TIMEOUT = 500;

export function getCcsSettings(): CcsSettings {
const configuration = vscode.workspace.getConfiguration(CCS_CONFIGURATION_SECTION);
const endpoint = sanitizeEndpoint(configuration.get<string | undefined>("endpoint"));
const requestTimeout = coerceTimeout(configuration.get<number | undefined>("requestTimeout"));
const debugLogging = Boolean(configuration.get<boolean | undefined>("debugLogging"));
const flags = configuration.get<Record<string, boolean>>("flags") ?? {};

return {
endpoint,
requestTimeout,
debugLogging,
flags,
};
}

export function isFlagEnabled(flag: string, settings: CcsSettings = getCcsSettings()): boolean {
return Boolean(settings.flags?.[flag]);
}

function sanitizeEndpoint(endpoint?: string): string | undefined {
if (!endpoint) {
return undefined;
}

const trimmed = endpoint.trim();
if (!trimmed) {
return undefined;
}

return trimmed.replace(/\/+$/, "");
}

function coerceTimeout(timeout: number | undefined): number {
if (typeof timeout !== "number" || Number.isNaN(timeout)) {
return DEFAULT_TIMEOUT;
}

return Math.max(0, Math.floor(timeout));
}
81 changes: 81 additions & 0 deletions src/ccs/core/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from "axios";
import * as https from "https";
import * as vscode from "vscode";

import { logDebug, logError } from "./logging";
import { getCcsSettings } from "../config/settings";

interface CreateClientOptions {
baseURL: string;
auth?: AxiosRequestConfig["auth"];
defaultTimeout?: number;
}

export function createHttpClient(options: CreateClientOptions): AxiosInstance {
const { baseURL, auth, defaultTimeout } = options;
const strictSSL = vscode.workspace.getConfiguration("http").get<boolean>("proxyStrictSSL");
const httpsAgent = new https.Agent({ rejectUnauthorized: strictSSL });
const timeout = typeof defaultTimeout === "number" ? defaultTimeout : getCcsSettings().requestTimeout;

const client = axios.create({
baseURL,
auth,
timeout,
headers: { "Content-Type": "application/json" },
httpsAgent,
});

attachLogging(client);

return client;
}

function attachLogging(client: AxiosInstance): void {
client.interceptors.request.use((config) => {
logDebug(`HTTP ${config.method?.toUpperCase()} ${resolveFullUrl(client, config)}`);
return config;
});

client.interceptors.response.use(
(response) => {
logDebug(`HTTP ${response.status} ${resolveFullUrl(client, response.config)}`);
return response;
},
(error: AxiosError) => {
if (axios.isCancel(error)) {
logDebug("HTTP request cancelled");
return Promise.reject(error);
}

const status = error.response?.status;
const url = resolveFullUrl(client, error.config ?? {});
const message = typeof status === "number" ? `HTTP ${status} ${url}` : `HTTP request failed ${url}`;
logError(message, error);
return Promise.reject(error);
}
);
}

function resolveFullUrl(client: AxiosInstance, config: AxiosRequestConfig | InternalAxiosRequestConfig): string {
const base = config.baseURL ?? client.defaults.baseURL ?? "";
const url = config.url ?? "";
if (!base) {
return url;
}

if (/^https?:/i.test(url)) {
return url;
}

return `${base}${url}`;
}

export function createAbortSignal(token: vscode.CancellationToken): { signal: AbortSignal; dispose: () => void } {
const controller = new AbortController();
const subscription = token.onCancellationRequested(() => controller.abort());

return {
signal: controller.signal,
dispose: () => subscription.dispose(),
};
}
52 changes: 52 additions & 0 deletions src/ccs/core/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { inspect } from "util";

import { outputChannel } from "../../utils";
import { getCcsSettings } from "../config/settings";

type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";

const PREFIX = "[CCS]";

export function logDebug(message: string, ...details: unknown[]): void {
if (!getCcsSettings().debugLogging) {
return;
}
writeLog("DEBUG", message, details);
}

export function logInfo(message: string, ...details: unknown[]): void {
writeLog("INFO", message, details);
}

export function logWarn(message: string, ...details: unknown[]): void {
writeLog("WARN", message, details);
}

export function logError(message: string, error?: unknown): void {
const details = error ? [formatError(error)] : [];
writeLog("ERROR", message, details);
}

function writeLog(level: LogLevel, message: string, details: unknown[]): void {
const timestamp = new Date().toISOString();
outputChannel.appendLine(`${PREFIX} ${timestamp} ${level}: ${message}`);
if (details.length > 0) {
for (const detail of details) {
outputChannel.appendLine(`${PREFIX} ${stringify(detail)}`);
}
}
}

function stringify(value: unknown): string {
if (typeof value === "string") {
return value;
}
return inspect(value, { depth: 4, breakLength: Infinity });
}

function formatError(error: unknown): string {
if (error instanceof Error) {
return `${error.name}: ${error.message}${error.stack ? `\n${error.stack}` : ""}`;
}
return stringify(error);
}
10 changes: 10 additions & 0 deletions src/ccs/core/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface ResolveContextExpressionResponse {
status?: string;
textExpression?: string;
message?: string;
}

export interface SourceControlError {
message: string;
cause?: unknown;
}
5 changes: 5 additions & 0 deletions src/ccs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { getCcsSettings, isFlagEnabled, type CcsSettings } from "./config/settings";
export { logDebug, logError, logInfo, logWarn } from "./core/logging";
export { SourceControlApi } from "./sourcecontrol/client";
export { resolveContextExpression } from "./commands/contextHelp";
export { ContextExpressionClient } from "./sourcecontrol/clients/contextExpressionClient";
57 changes: 57 additions & 0 deletions src/ccs/sourcecontrol/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";

import { AtelierAPI } from "../../api";
import { getCcsSettings } from "../config/settings";
import { createHttpClient } from "../core/http";
import { logDebug } from "../core/logging";
import { BASE_PATH } from "./routes";

export class SourceControlApi {
private readonly client: AxiosInstance;

private constructor(client: AxiosInstance) {
this.client = client;
}

public static fromAtelierApi(api: AtelierAPI): SourceControlApi {
const { host, port, username, password, https: useHttps, pathPrefix } = api.config;

if (!host || !port) {
throw new Error("No active InterSystems server connection for this file.");
}

const normalizedPrefix = pathPrefix ? (pathPrefix.startsWith("/") ? pathPrefix : `/${pathPrefix}`) : "";
const trimmedPrefix = normalizedPrefix.endsWith("/") ? normalizedPrefix.slice(0, -1) : normalizedPrefix;
const encodedPrefix = encodeURI(trimmedPrefix);
const protocol = useHttps ? "https" : "http";
const defaultBaseUrl = `${protocol}://${host}:${port}${encodedPrefix}${BASE_PATH}`;

const { endpoint, requestTimeout } = getCcsSettings();
const baseURL = endpoint ?? defaultBaseUrl;
const auth =
typeof username === "string" && typeof password === "string"
? {
username,
password,
}
: undefined;

logDebug("Creating SourceControl API client", { baseURL, hasAuth: Boolean(auth) });

const client = createHttpClient({
baseURL,
auth,
defaultTimeout: requestTimeout,
});

return new SourceControlApi(client);
}

public post<T = unknown, R = AxiosResponse<T>>(
route: string,
data?: unknown,
config?: AxiosRequestConfig<unknown>
): Promise<R> {
return this.client.post<T, R>(route, data, config);
}
}
Loading