Skip to content

Commit bd7bddd

Browse files
Merge branch 'prerelease' of https://github.com/consistem/vscode-objectscript into prerelease
2 parents 8d867c0 + f810825 commit bd7bddd

File tree

12 files changed

+335
-75
lines changed

12 files changed

+335
-75
lines changed

src/api/ccs/sourceControl.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

src/commands/ccs/contextHelp.ts renamed to src/ccs/commands/contextHelp.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
import * as path from "path";
22
import * as vscode from "vscode";
33

4-
import { AtelierAPI } from "../../api";
5-
import { SourceControlApi } from "../../api/ccs/sourceControl";
4+
import { ContextExpressionClient } from "../sourcecontrol/clients/contextExpressionClient";
65
import { handleError } from "../../utils";
76

8-
interface ResolveContextExpressionResponse {
9-
status?: string;
10-
textExpression?: string;
11-
message?: string;
12-
}
7+
const sharedClient = new ContextExpressionClient();
138

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

3025
const routine = path.basename(document.fileName);
31-
const api = new AtelierAPI(document.uri);
32-
33-
let sourceControlApi: SourceControlApi;
34-
try {
35-
sourceControlApi = SourceControlApi.fromAtelierApi(api);
36-
} catch (error) {
37-
void vscode.window.showErrorMessage(error instanceof Error ? error.message : String(error));
38-
return;
39-
}
4026

4127
try {
42-
const response = await sourceControlApi.post<ResolveContextExpressionResponse>("/resolveContextExpression", {
43-
routine,
44-
contextExpression,
45-
});
28+
const response = await sharedClient.resolve(document, { routine, contextExpression });
29+
const data = response ?? {};
4630

47-
const data = response.data ?? {};
4831
if (typeof data.status === "string" && data.status.toLowerCase() === "success" && data.textExpression) {
4932
const eol = document.eol === vscode.EndOfLine.CRLF ? "\r\n" : "\n";
5033
const textExpression = data.textExpression.replace(/\r?\n/g, eol);

src/ccs/config/schema.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Configuração do módulo CCS
2+
3+
As opções abaixo ficam no escopo `objectscript.ccs` e controlam as integrações específicas
4+
para o fork da Consistem.
5+
6+
| Chave | Tipo | Padrão | Descrição |
7+
| ---------------- | ------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------- |
8+
| `endpoint` | `string` | `undefined` | URL base alternativa para a API. Se não definida, a URL é derivada da conexão ativa do Atelier. |
9+
| `requestTimeout` | `number` | `500` | Tempo limite (ms) aplicado às chamadas HTTP do módulo. Valores menores ou inválidos são normalizados para zero. |
10+
| `debugLogging` | `boolean` | `false` | Quando verdadeiro, registra mensagens detalhadas no `ObjectScript` Output Channel. |
11+
| `flags` | `Record<string, boolean>` | `{}` | Feature flags opcionais que podem ser lidas pelas features do módulo. |
12+
13+
Essas configurações não exigem reload da janela; toda leitura é feita sob demanda.

src/ccs/config/settings.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as vscode from "vscode";
2+
3+
export interface CcsSettings {
4+
endpoint?: string;
5+
requestTimeout: number;
6+
debugLogging: boolean;
7+
flags: Record<string, boolean>;
8+
}
9+
10+
const CCS_CONFIGURATION_SECTION = "objectscript.ccs";
11+
const DEFAULT_TIMEOUT = 500;
12+
13+
export function getCcsSettings(): CcsSettings {
14+
const configuration = vscode.workspace.getConfiguration(CCS_CONFIGURATION_SECTION);
15+
const endpoint = sanitizeEndpoint(configuration.get<string | undefined>("endpoint"));
16+
const requestTimeout = coerceTimeout(configuration.get<number | undefined>("requestTimeout"));
17+
const debugLogging = Boolean(configuration.get<boolean | undefined>("debugLogging"));
18+
const flags = configuration.get<Record<string, boolean>>("flags") ?? {};
19+
20+
return {
21+
endpoint,
22+
requestTimeout,
23+
debugLogging,
24+
flags,
25+
};
26+
}
27+
28+
export function isFlagEnabled(flag: string, settings: CcsSettings = getCcsSettings()): boolean {
29+
return Boolean(settings.flags?.[flag]);
30+
}
31+
32+
function sanitizeEndpoint(endpoint?: string): string | undefined {
33+
if (!endpoint) {
34+
return undefined;
35+
}
36+
37+
const trimmed = endpoint.trim();
38+
if (!trimmed) {
39+
return undefined;
40+
}
41+
42+
return trimmed.replace(/\/+$/, "");
43+
}
44+
45+
function coerceTimeout(timeout: number | undefined): number {
46+
if (typeof timeout !== "number" || Number.isNaN(timeout)) {
47+
return DEFAULT_TIMEOUT;
48+
}
49+
50+
return Math.max(0, Math.floor(timeout));
51+
}

src/ccs/core/http.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from "axios";
2+
import * as https from "https";
3+
import * as vscode from "vscode";
4+
5+
import { logDebug, logError } from "./logging";
6+
import { getCcsSettings } from "../config/settings";
7+
8+
interface CreateClientOptions {
9+
baseURL: string;
10+
auth?: AxiosRequestConfig["auth"];
11+
defaultTimeout?: number;
12+
}
13+
14+
export function createHttpClient(options: CreateClientOptions): AxiosInstance {
15+
const { baseURL, auth, defaultTimeout } = options;
16+
const strictSSL = vscode.workspace.getConfiguration("http").get<boolean>("proxyStrictSSL");
17+
const httpsAgent = new https.Agent({ rejectUnauthorized: strictSSL });
18+
const timeout = typeof defaultTimeout === "number" ? defaultTimeout : getCcsSettings().requestTimeout;
19+
20+
const client = axios.create({
21+
baseURL,
22+
auth,
23+
timeout,
24+
headers: { "Content-Type": "application/json" },
25+
httpsAgent,
26+
});
27+
28+
attachLogging(client);
29+
30+
return client;
31+
}
32+
33+
function attachLogging(client: AxiosInstance): void {
34+
client.interceptors.request.use((config) => {
35+
logDebug(`HTTP ${config.method?.toUpperCase()} ${resolveFullUrl(client, config)}`);
36+
return config;
37+
});
38+
39+
client.interceptors.response.use(
40+
(response) => {
41+
logDebug(`HTTP ${response.status} ${resolveFullUrl(client, response.config)}`);
42+
return response;
43+
},
44+
(error: AxiosError) => {
45+
if (axios.isCancel(error)) {
46+
logDebug("HTTP request cancelled");
47+
return Promise.reject(error);
48+
}
49+
50+
const status = error.response?.status;
51+
const url = resolveFullUrl(client, error.config ?? {});
52+
const message = typeof status === "number" ? `HTTP ${status} ${url}` : `HTTP request failed ${url}`;
53+
logError(message, error);
54+
return Promise.reject(error);
55+
}
56+
);
57+
}
58+
59+
function resolveFullUrl(client: AxiosInstance, config: AxiosRequestConfig | InternalAxiosRequestConfig): string {
60+
const base = config.baseURL ?? client.defaults.baseURL ?? "";
61+
const url = config.url ?? "";
62+
if (!base) {
63+
return url;
64+
}
65+
66+
if (/^https?:/i.test(url)) {
67+
return url;
68+
}
69+
70+
return `${base}${url}`;
71+
}
72+
73+
export function createAbortSignal(token: vscode.CancellationToken): { signal: AbortSignal; dispose: () => void } {
74+
const controller = new AbortController();
75+
const subscription = token.onCancellationRequested(() => controller.abort());
76+
77+
return {
78+
signal: controller.signal,
79+
dispose: () => subscription.dispose(),
80+
};
81+
}

src/ccs/core/logging.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { inspect } from "util";
2+
3+
import { outputChannel } from "../../utils";
4+
import { getCcsSettings } from "../config/settings";
5+
6+
type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";
7+
8+
const PREFIX = "[CCS]";
9+
10+
export function logDebug(message: string, ...details: unknown[]): void {
11+
if (!getCcsSettings().debugLogging) {
12+
return;
13+
}
14+
writeLog("DEBUG", message, details);
15+
}
16+
17+
export function logInfo(message: string, ...details: unknown[]): void {
18+
writeLog("INFO", message, details);
19+
}
20+
21+
export function logWarn(message: string, ...details: unknown[]): void {
22+
writeLog("WARN", message, details);
23+
}
24+
25+
export function logError(message: string, error?: unknown): void {
26+
const details = error ? [formatError(error)] : [];
27+
writeLog("ERROR", message, details);
28+
}
29+
30+
function writeLog(level: LogLevel, message: string, details: unknown[]): void {
31+
const timestamp = new Date().toISOString();
32+
outputChannel.appendLine(`${PREFIX} ${timestamp} ${level}: ${message}`);
33+
if (details.length > 0) {
34+
for (const detail of details) {
35+
outputChannel.appendLine(`${PREFIX} ${stringify(detail)}`);
36+
}
37+
}
38+
}
39+
40+
function stringify(value: unknown): string {
41+
if (typeof value === "string") {
42+
return value;
43+
}
44+
return inspect(value, { depth: 4, breakLength: Infinity });
45+
}
46+
47+
function formatError(error: unknown): string {
48+
if (error instanceof Error) {
49+
return `${error.name}: ${error.message}${error.stack ? `\n${error.stack}` : ""}`;
50+
}
51+
return stringify(error);
52+
}

src/ccs/core/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface ResolveContextExpressionResponse {
2+
status?: string;
3+
textExpression?: string;
4+
message?: string;
5+
}
6+
7+
export interface SourceControlError {
8+
message: string;
9+
cause?: unknown;
10+
}

src/ccs/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { getCcsSettings, isFlagEnabled, type CcsSettings } from "./config/settings";
2+
export { logDebug, logError, logInfo, logWarn } from "./core/logging";
3+
export { SourceControlApi } from "./sourcecontrol/client";
4+
export { resolveContextExpression } from "./commands/contextHelp";
5+
export { ContextExpressionClient } from "./sourcecontrol/clients/contextExpressionClient";

src/ccs/sourcecontrol/client.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
2+
3+
import { AtelierAPI } from "../../api";
4+
import { getCcsSettings } from "../config/settings";
5+
import { createHttpClient } from "../core/http";
6+
import { logDebug } from "../core/logging";
7+
import { BASE_PATH } from "./routes";
8+
9+
export class SourceControlApi {
10+
private readonly client: AxiosInstance;
11+
12+
private constructor(client: AxiosInstance) {
13+
this.client = client;
14+
}
15+
16+
public static fromAtelierApi(api: AtelierAPI): SourceControlApi {
17+
const { host, port, username, password, https: useHttps, pathPrefix } = api.config;
18+
19+
if (!host || !port) {
20+
throw new Error("No active InterSystems server connection for this file.");
21+
}
22+
23+
const normalizedPrefix = pathPrefix ? (pathPrefix.startsWith("/") ? pathPrefix : `/${pathPrefix}`) : "";
24+
const trimmedPrefix = normalizedPrefix.endsWith("/") ? normalizedPrefix.slice(0, -1) : normalizedPrefix;
25+
const encodedPrefix = encodeURI(trimmedPrefix);
26+
const protocol = useHttps ? "https" : "http";
27+
const defaultBaseUrl = `${protocol}://${host}:${port}${encodedPrefix}${BASE_PATH}`;
28+
29+
const { endpoint, requestTimeout } = getCcsSettings();
30+
const baseURL = endpoint ?? defaultBaseUrl;
31+
const auth =
32+
typeof username === "string" && typeof password === "string"
33+
? {
34+
username,
35+
password,
36+
}
37+
: undefined;
38+
39+
logDebug("Creating SourceControl API client", { baseURL, hasAuth: Boolean(auth) });
40+
41+
const client = createHttpClient({
42+
baseURL,
43+
auth,
44+
defaultTimeout: requestTimeout,
45+
});
46+
47+
return new SourceControlApi(client);
48+
}
49+
50+
public post<T = unknown, R = AxiosResponse<T>>(
51+
route: string,
52+
data?: unknown,
53+
config?: AxiosRequestConfig<unknown>
54+
): Promise<R> {
55+
return this.client.post<T, R>(route, data, config);
56+
}
57+
}

0 commit comments

Comments
 (0)