Skip to content

Commit 1ba6133

Browse files
reorganization to make it possible to run in the browser
1 parent b544377 commit 1ba6133

10 files changed

+110
-29
lines changed

eslint.config.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ import tseslint from "typescript-eslint";
66
export default tseslint.config(
77
eslint.configs.recommended,
88
...tseslint.configs.recommended,
9-
{ignores: ["**/.venv/", "**/es-client/", "tests/wasm"]},
9+
{ignores: ["**/.venv/", "**/es-client/", "**/templates.js", "tests/wasm"]},
1010
{languageOptions: {globals: {require: true}}},
1111
);

jest.config.js

+5
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,10 @@ module.exports = {
33
testEnvironment: "node",
44
setupFilesAfterEnv: [
55
"jest-expect-message"
6+
],
7+
moduleFileExtensions: [
8+
"ts",
9+
"js",
10+
"tpl"
611
]
712
};

package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
"scripts": {
1212
"update-schema": "node scripts/update-schema.mjs",
1313
"release": "./scripts/release.sh",
14-
"build": "npm run clean && tsc && npm run copy-files",
14+
"build": "npm run clean && tsc && npm run compile-templates && npm run copy-files",
1515
"clean": "rimraf dist/",
16-
"copy-files": "copyfiles -u 1 src/**/*.js src/**/*.json src/**/*.tpl dist/",
16+
"compile-templates": "node scripts/compile-templates.mjs",
17+
"copy-files": "copyfiles -u 1 src/**/*.js src/**/*.json dist/",
1718
"docs": "typedoc src/index.ts",
1819
"lint": "eslint src tests --ignore-pattern tests/wasm/",
1920
"prettier": "prettier \"src/**/*.ts\" \"tests/**/*.ts\" --list-different",
20-
"test": "jest --test-path-ignore-patterns integration --coverage",
21+
"test": "npm run compile-templates && jest --test-path-ignore-patterns integration --coverage",
2122
"test:setup": "node scripts/download-examples.mjs && ./tests/integration/run-python.sh && ./tests/integration/run-javascript.sh",
2223
"test:setup-curl": "node scripts/download-examples.mjs",
2324
"test:setup-python": "node scripts/download-examples.mjs && ./tests/integration/run-python.sh",
@@ -31,8 +32,7 @@
3132
"fix:lint": "eslint src tests --fix --ignore-pattern tests/wasm/",
3233
"fix:prettier": "prettier \"src/**/*.ts\" \"tests/**/*.ts\" --write",
3334
"fix:precommit": "./scripts/precommit.sh",
34-
"watch:build": "tsc -w",
35-
"watch:test": "jest --test-path-ignore-patterns integration --coverage --watch",
35+
"watch:test": "npm run compile-templates && jest --test-path-ignore-patterns integration --coverage --watch",
3636
"prepare": "husky install"
3737
},
3838
"main": "dist/index.js",

scripts/compile-templates.mjs

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import fs from 'fs';
2+
import { exec } from 'child_process';
3+
4+
const cmd = (process.platform !== "win32")
5+
? "handlebars src/exporters/*.tpl -f src/exporters/templates.js -c"
6+
: "handlebars src\\exporters\\*.tpl -f src\\exporters\\templates.js -c";
7+
exec(cmd, () => {
8+
let templates = fs.readFileSync('src/exporters/templates.js', 'utf-8');
9+
templates = 'const Handlebars = require("handlebars");\n' + templates;
10+
fs.writeFileSync('src/exporters/templates.js', templates);
11+
});

src/convert.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { CurlExporter } from "./exporters/curl";
66
import { JavaScriptExporter } from "./exporters/javascript";
77
import util from "util";
88

9-
const execAsync = util.promisify(childProcess.exec);
9+
const isBrowser = typeof window !== "undefined";
10+
const execAsync = !isBrowser ? util.promisify(childProcess.exec) : undefined;
1011

1112
export type ConvertOptions = {
1213
/** When `true`, the converter will only check if the conversion can be carried
@@ -188,6 +189,9 @@ export class SubprocessExporter implements FormatExporter {
188189
const input = base64url.encode(
189190
JSON.stringify({ requests: getSlimRequests(requests) }),
190191
);
192+
if (execAsync === undefined) {
193+
throw new Error("Cannot use exec()");
194+
}
191195
const { stdout, stderr } = await execAsync(
192196
`${this.baseCmd} check ${input}`,
193197
);
@@ -208,6 +212,9 @@ export class SubprocessExporter implements FormatExporter {
208212
const input = base64url.encode(
209213
JSON.stringify({ requests: getSlimRequests(requests), options }),
210214
);
215+
if (execAsync === undefined) {
216+
throw new Error("Cannot use exec()");
217+
}
211218
const { stdout } = await execAsync(`${this.baseCmd} convert ${input}`);
212219
if (stdout) {
213220
const json = JSON.parse(base64url.decode(stdout));

src/exporters/javascript.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { readFileSync } from "fs";
22
import path from "path";
33
import Handlebars from "handlebars";
44
import prettier from "prettier";
5+
import prettierTypeScript from "prettier/parser-typescript";
56
import { FormatExporter, ConvertOptions } from "../convert";
67
import { ParsedRequest } from "../parse";
8+
import "./templates";
79

810
const UNSUPPORTED_APIS = new RegExp(
911
"^query_rules.*$" +
@@ -39,7 +41,10 @@ export class JavaScriptExporter implements FormatExporter {
3941
throw new Error("Cannot perform conversion");
4042
}
4143
const output = this.template({ requests, ...options });
42-
return prettier.format(output, { parser: "typescript" });
44+
return prettier.format(output, {
45+
parser: "typescript",
46+
plugins: [prettierTypeScript],
47+
});
4348
}
4449

4550
get template(): Handlebars.TemplateDelegate {
@@ -115,8 +120,17 @@ export class JavaScriptExporter implements FormatExporter {
115120

116121
Handlebars.registerHelper("camelCase", (text) => toCamelCase(text));
117122

118-
const t = readFileSync(path.join(__dirname, "./javascript.tpl"), "utf-8");
119-
this._template = Handlebars.compile(t);
123+
if (process.env.NODE_ENV !== "test") {
124+
this._template = Handlebars.templates["javascript.tpl"];
125+
} else {
126+
// when running tests we read the templates directly, in case the
127+
// compiled file is not up to date
128+
const t = readFileSync(
129+
path.join(__dirname, "./javascript.tpl"),
130+
"utf-8",
131+
);
132+
this._template = Handlebars.compile(t);
133+
}
120134
}
121135

122136
return this._template;

src/exporters/python.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { readFile } from "fs/promises";
1+
import { readFileSync } from "fs";
22
import path from "path";
33
import Handlebars from "handlebars";
44
import { FormatExporter, ConvertOptions } from "../convert";
55
import { ParsedRequest } from "../parse";
6+
import "./templates";
67

78
// this regex should match the list of APIs that do not have specific handlers
89
// in the Python client. APIs in this list are rendered with a perform_request()
@@ -159,10 +160,14 @@ export class PythonExporter implements FormatExporter {
159160
},
160161
);
161162

162-
const t = await readFile(path.join(__dirname, "./python.tpl"), {
163-
encoding: "utf-8",
164-
});
165-
this.template = Handlebars.compile(t);
163+
if (process.env.NODE_ENV !== "test") {
164+
this.template = Handlebars.templates["python.tpl"];
165+
} else {
166+
// when running tests we read the templates directly, in case the
167+
// compiled file is not up to date
168+
const t = readFileSync(path.join(__dirname, "./python.tpl"), "utf-8");
169+
this.template = Handlebars.compile(t);
170+
}
166171
}
167172
return this.template;
168173
}

src/parse.ts

+31-14
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { METHODS } from "http";
21
import { URL } from "url";
32
import { readFile } from "fs/promises";
43
import path from "path";
54
import * as Router from "find-my-way-ts";
65
import { Model, Request } from "./metamodel";
76

7+
const isBrowser = typeof window !== "undefined";
8+
const httpMethods = ["HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"];
89
export type JSONValue = string | number | boolean | JSONArray | JSONObject;
910
interface JSONArray extends Array<JSONValue> {}
1011
interface JSONObject {
@@ -47,7 +48,7 @@ type ESRoute = {
4748
request: Request;
4849
};
4950

50-
const router = Router.make<ESRoute>({
51+
let router = Router.make<ESRoute>({
5152
ignoreTrailingSlash: true,
5253
maxParamLength: 1000,
5354
});
@@ -62,7 +63,7 @@ export function splitSource(source: string): string[] {
6263
let prev = 0;
6364
while (index < len) {
6465
// Beginning of a new command, we should find the method and proceede to the url.
65-
for (const method of METHODS) {
66+
for (const method of httpMethods) {
6667
if (source.slice(index, len).startsWith(method)) {
6768
index += method.length;
6869
break;
@@ -80,7 +81,7 @@ export function splitSource(source: string): string[] {
8081
if (index == len) return;
8182
let brackets = 0;
8283
// If we found an http method, then we have found a new command.
83-
for (const method of METHODS) {
84+
for (const method of httpMethods) {
8485
if (source.slice(index, len).startsWith(method)) {
8586
return;
8687
}
@@ -90,7 +91,7 @@ export function splitSource(source: string): string[] {
9091
// If we find an open curly bracket, we should also find the closing one
9192
// before to checking for the http method.
9293
if (source[index] == "{") {
93-
for (;;) {
94+
for (; index < len; ) {
9495
if (source[index] == "{") {
9596
brackets += 1;
9697
} else if (source[index] == "}") {
@@ -131,7 +132,7 @@ function parseCommand(source: string, options: ParseOptions) {
131132
const len = source.length;
132133
let index = 0;
133134
// identify the method
134-
for (const method of METHODS) {
135+
for (const method of httpMethods) {
135136
if (source.slice(index, len).startsWith(method)) {
136137
data.method = method;
137138
index += method.length;
@@ -264,16 +265,28 @@ function parseCommand(source: string, options: ParseOptions) {
264265
* function directly, but it can be used to load a different version of the
265266
* specification than the one bundled with this package.
266267
*
267-
* @param filename The path to the schema.json file to load.
268+
* @param filename_or_object The path to the schema.json file to load, or an
269+
* object with a loaded schema.
268270
*/
269-
export async function loadSchema(filename: string) {
271+
export async function loadSchema(filename_or_object: string | object) {
272+
let spec: Model;
273+
274+
if (typeof filename_or_object === "string") {
275+
spec = JSON.parse(
276+
await readFile(filename_or_object, { encoding: "utf-8" }),
277+
) as Model;
278+
} else {
279+
spec = filename_or_object as Model;
280+
}
281+
270282
if (router.find("GET", "/") != undefined) {
271-
throw Error("A schema has already been loaded");
283+
// start from a clean router
284+
router = Router.make<ESRoute>({
285+
ignoreTrailingSlash: true,
286+
maxParamLength: 1000,
287+
});
272288
}
273289

274-
const spec = JSON.parse(
275-
await readFile(filename, { encoding: "utf-8" }),
276-
) as Model;
277290
for (const endpoint of spec.endpoints) {
278291
for (const url of endpoint.urls) {
279292
const { path, methods } = url;
@@ -323,8 +336,12 @@ async function getAPI(
323336
endpointPath: string,
324337
): Promise<Router.FindResult<ESRoute>> {
325338
if (router.find("GET", "/") == undefined) {
326-
// load the Elasticsearch spec
327-
await loadSchema(path.join(__dirname, "./schema.json"));
339+
if (!isBrowser) {
340+
// load the Elasticsearch spec
341+
await loadSchema(path.join(__dirname, "./schema.json"));
342+
} else {
343+
throw new Error("Specification is missing");
344+
}
328345
}
329346

330347
const formattedPath = endpointPath.startsWith("/")

tests/parse.test.ts

+21
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,27 @@ POST\n_ml/anomaly_detectors/it_ops_new_logs/model_snapshots/1491852978/_update\n
273273
});
274274
});
275275

276+
it("errors with incomplete bodies", async () => {
277+
const script = `GET /my-index/_search
278+
{`;
279+
expect(async () => await parseRequests(script)).rejects.toThrowError(
280+
"body cannot be parsed",
281+
);
282+
const req = await parseRequest(script, { ignoreErrors: true });
283+
expect(req).toMatchObject({
284+
source: "GET /my-index/_search\n{",
285+
service: "es",
286+
api: "search",
287+
body: "\n{",
288+
method: "GET",
289+
params: {
290+
index: "my-index",
291+
},
292+
path: "/my-index/_search",
293+
rawPath: "/my-index/_search",
294+
});
295+
});
296+
276297
it("parses kibana requests", async () => {
277298
const req = await parseRequest(
278299
"GET kbn:/api/saved_objects/_find?type=dashboard",

tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"esModuleInterop": true,
88
"strictPropertyInitialization": false,
99
"strict": true,
10+
"allowJs": true,
1011
"rootDir": "./src",
1112
"outDir": "./dist"
1213
},

0 commit comments

Comments
 (0)