This is an automated email from the ASF dual-hosted git repository.

sciabarracom pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/openserverless-configurator.git

commit cd8b30d370f073956ac8a5d4bcc22c9adaf13644
Author: Michele Sciabarra <[email protected]>
AuthorDate: Mon Sep 28 20:54:14 2026 +0100

    configurator sources
---
 README.md                                      |  58 ++++
 configurator/bun.lock                          |  41 +++
 configurator/configurator.ts                   | 389 +++++++++++++++++++++++++
 configurator/index.ts                          |  22 ++
 configurator/package.json                      |  19 ++
 configurator/tests/fixtures/bad.json           |   1 +
 configurator/tests/fixtures/multi-keys.json    |  22 ++
 configurator/tests/fixtures/not-json.txt       |  16 +
 configurator/tests/fixtures/valid.json         |  11 +
 configurator/tests/fixtures/with-defaults.json |  26 ++
 configurator/tests/index.test.ts               | 144 +++++++++
 configurator/tsconfig.json                     |  27 ++
 12 files changed, 776 insertions(+)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d1e3c35
--- /dev/null
+++ b/README.md
@@ -0,0 +1,58 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+## Openserverless Configurator task
+
+### Development:
+
+The configurator is inside the `configurator` folder.
+The entrypoint is the `configurator/index.ts` file. It simply calls the 
configurator main function.
+
+All the logic is inside the `configurator/configurator.ts` file. Of course, 
you can add more files
+and change the structure as you see fit.
+
+Inside the `configurator` folder, first install dependencies:
+
+```bash
+bun install
+```
+
+In the `package.json` file there are a couple of scripts. The `start` script 
will run the configurator:
+
+```bash
+bun run start
+```
+
+### Tests:
+
+There are unit tests inside the `configurator/test` folder. You can run them 
with the following command:
+
+```bash
+bun run test
+```
+
+### Building:
+
+To build the configurator and generate the `configurator.js` file, run the 
following command:
+
+```bash
+bun run build
+```
+
+It will generate the `configurator.js` file and move it in the parent, where 
it can be used by the opsfile task.
diff --git a/configurator/bun.lock b/configurator/bun.lock
new file mode 100644
index 0000000..9031cbc
--- /dev/null
+++ b/configurator/bun.lock
@@ -0,0 +1,41 @@
+{
+  "lockfileVersion": 1,
+  "configVersion": 0,
+  "workspaces": {
+    "": {
+      "name": "prompt",
+      "dependencies": {
+        "@clack/prompts": "^0.7.0",
+      },
+      "devDependencies": {
+        "@types/bun": "latest",
+      },
+      "peerDependencies": {
+        "typescript": "^5.0.0",
+      },
+    },
+  },
+  "packages": {
+    "@clack/core": ["@clack/[email protected]", "", { "dependencies": { "picocolors": 
"^1.0.0", "sisteransi": "^1.0.5" } }, 
"sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw=="],
+
+    "@clack/prompts": ["@clack/[email protected]", "", { "dependencies": { 
"@clack/core": "^0.3.3", "is-unicode-supported": "*", "picocolors": "^1.0.0", 
"sisteransi": "^1.0.5" } }, 
"sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA=="],
+
+    "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": 
"1.1.17" } }, 
"sha512-uJgKjTdX0GkWEHZzQzFsJkWp5+43ZS7HC8sZPFnOwnSo1AsNl2q9o2bFeS23disNDqbggEgyFkKCHl/w8iZsMA=="],
+
+    "@types/node": ["@types/[email protected]", "", { "dependencies": { 
"undici-types": "~5.26.4" } }, 
"sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
+
+    "@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": 
"*" } }, 
"sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ=="],
+
+    "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": 
"~20.12.8", "@types/ws": "~8.5.10" } }, 
"sha512-Z4+OplcSd/YZq7ZsrfD00DKJeCwuNY96a1IDJyR73+cTBaFIS7SC6LhpY/W3AMEXO9iYq5NJ58WAwnwL1p5vKg=="],
+
+    "is-unicode-supported": ["[email protected]", "", {}, 
"sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q=="],
+
+    "picocolors": ["[email protected]", "", {}, 
"sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="],
+
+    "sisteransi": ["[email protected]", "", {}, 
"sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
+
+    "typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", 
"tsserver": "bin/tsserver" } }, 
"sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q=="],
+
+    "undici-types": ["[email protected]", "", {}, 
"sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
+  }
+}
diff --git a/configurator/configurator.ts b/configurator/configurator.ts
new file mode 100644
index 0000000..70a5f13
--- /dev/null
+++ b/configurator/configurator.ts
@@ -0,0 +1,389 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { $ } from "bun";
+
+import {
+  intro,
+  outro,
+  select,
+  isCancel,
+  cancel,
+  text,
+  password,
+} from "@clack/prompts";
+import color from "picocolors";
+
+import { parseArgs } from "util";
+
+type Prompts = Record<string, PromptData>;
+
+type PromptData = {
+  label?: string;
+  type: string | string[];
+  default?: string;
+  value?: string;
+};
+
+const OPS = process.env.OPS || "ops";
+
+export const AdditionalArgsMsg = "Additional arguments will be ignored.";
+export const NotValidJsonMsg = "Not a valid JSON file";
+export const FileNotFoundJsonMsg = "The JSON file was not found. Pathname: ";
+export const BadConfigMsg =
+  "Bad configuration file. Check the help message (-h) to see the expected 
format.";
+
+export const HelpMsg = `
+Usage: config <configjson> [-o | --override]
+
+Description:
+Prompt the user for configuration data defined in the config.json file. 
+The script will ignore the keys from the input config that are already set in 
ops
+and only prompt the user for the missing data, unless the override flag is 
set. 
+Then they will be saved in the ops config. You can also define a default value.
+
+The config.json file must be a JSON file with the following structure:
+      
+  {
+    "KEY": {
+      "type": "string"
+    },
+    "OTHER_KEY": {
+      "label": "An optional custom message",
+      "type": "int",
+      "default": "42"
+    },
+    ...
+  }
+
+The keys must be uppercase words (separated by underscores).
+The value for the "type" key must be either string with the following values:
+- string
+- int
+- float
+- bool
+- password
+- an array of strings with specific values (an enum).
+`;
+
+export default async function main() {
+  const options: { showhelp: { type: 'boolean' }, override: { type: 'boolean' 
} } = {
+    showhelp: {
+      type: 'boolean',
+    },
+    override: {
+      type: 'boolean',
+    },
+  };
+  const { values, positionals } = parseArgs({
+    args: Bun.argv,
+    options: options,
+    strict: true,
+    allowPositionals: true,
+
+  });
+
+  if (values.showhelp) {
+    console.log(HelpMsg);
+    return process.exit(0);
+  }
+
+  const override = values.override || false;
+
+  // 1. Read input config json
+  const readPosRes = readPositionalFile(positionals);
+  if (!readPosRes.success) {
+    cancel(readPosRes.message);
+    return process.exit(1);
+  }
+
+  if (readPosRes.help) {
+    console.log(readPosRes.help);
+    return process.exit(0);
+  }
+
+  if (readPosRes.message) {
+    console.warn(readPosRes.message);
+  }
+
+  // 2. Parse the json
+  const jsonRes = await parsePositionalFile(readPosRes.jsonFilePath!);
+
+  if (!jsonRes.success) {
+    cancel(jsonRes.message);
+    return process.exit(1);
+  }
+
+  const config = jsonRes.body;
+
+  // 3. Validate the given config json
+  if (!isInputConfigValid(config)) {
+    cancel(BadConfigMsg);
+    return process.exit(1);
+  }
+
+  // 4. Run OPS to get the available config data
+  const { exitCode, stderr, stdout } = await $`${OPS} -config -d`.quiet();
+  if (exitCode !== 0) {
+    cancel(stderr.toString());
+    return process.exit(1);
+  }
+
+  const opsCurrentConfig = stdout.toString();
+
+  // 5. Remove the keys from config that are already in the opsConfig
+  let missingData = config;
+  if (!override) {
+    missingData = findMissingConfig(config, opsCurrentConfig);
+  }
+
+  // 6. Ask the user for the missing data
+  console.log();
+  intro(color.inverse(" ops configurator "));
+
+  await askMissingData(missingData, override);
+
+  // 7. Save the data to the config?
+  console.log();
+
+  outro("You're all set!");
+}
+
+async function askMissingData(missingData: Prompts, override: boolean = false) 
{
+  if (Object.keys(missingData).length === 0) {
+    outro("Configuration set from ops");
+    process.exit(0);
+  }
+  console.log();
+
+  if (!override) {
+    console.log("Configuration partially set from ops. Need a few more:");
+  }
+
+  for (const key in missingData) {
+    let inputFromPrompt: string;
+    const prompt: PromptData = missingData[key];
+    // let askedForPassword = false;
+
+    if (Array.isArray(prompt.type)) {
+      const defaultValueOK = prompt.default && 
prompt.type.includes(prompt.default);
+      if (prompt.default && !defaultValueOK) {
+        console.log();
+        console.warn(`The default value ${prompt.default} is not in the enum 
values.`);
+      }
+
+      const selected = await select({
+        message: prompt.label || `Pick a value for '${key}'`,
+        options: prompt.type.map((v) => ({ label: v, value: v })),
+        initialValue: defaultValueOK ? prompt.default : undefined,
+      });
+
+      if (!selected || isCancel(selected)) {
+        cancel("Operation cancelled");
+        process.exit(0);
+      }
+
+      inputFromPrompt = selected.toString();
+
+      // inputConfigs[key] = { ...prompt, value: selected.toString() };
+    } else if (prompt.type === "bool") {
+      const selected = await select({
+        initialValue: prompt.default === "true" || prompt.default as unknown 
as boolean === true ? "true" : "false",
+        message: prompt.label || `Pick a true/false for '${key}'`,
+        options: [
+          { label: "true", value: "true" },
+          { label: "false", value: "false" },
+        ],
+      });
+
+      if (!selected || isCancel(selected)) {
+        cancel("Operation cancelled");
+        process.exit(0);
+      }
+
+      inputFromPrompt = selected.toString();
+      // inputConfigs[key] = { ...prompt, value: selected.toString() };
+    } else if (prompt.type === "password") {
+      if (prompt.default) {
+        console.log();
+        console.warn("Default password value is not supported. Please enter 
the password manually.");
+      }
+      const input = await password({
+        message: prompt.label || `Enter password value for ${key}`,
+        validate: (value) => {
+          if (!value) {
+            return "Password cannot be empty";
+          }
+        }
+      });
+
+      if (isCancel(input)) {
+        cancel("Operation cancelled");
+        process.exit(0);
+      }
+      inputFromPrompt = input;
+      // askedForPassword = true;
+    } else {
+      const defaultMsgFragment = prompt.default ? `(default: 
${prompt.default})` : "";
+      const message = `Enter value for ${key} ${defaultMsgFragment} 
(${prompt.type})`;
+
+      let input = await text({
+        message: prompt.label || message,
+        // defaultValue: prompt.default?.toString(),
+        initialValue: prompt.default?.toString(),
+        validate: (value) => {
+          switch (prompt.type) {
+            case "int":
+              if (!Number.isInteger(Number(value))) {
+                return `Value for ${key} must be an integer number`;
+              }
+              break;
+            case "float":
+              if (!Number(value)) {
+                return `Value for ${key} must be a number`;
+              }
+          }
+          return;
+        }
+      }) as string;
+
+      if (isCancel(input)) {
+        cancel("Operation cancelled");
+        process.exit(0);
+      }
+
+
+      inputFromPrompt = input;
+      // inputConfigs[key] = { ...prompt, value: input };
+    }
+
+    // if (!askedForPassword) {
+    //   console.log("Setting", key, "to", inputFromPrompt);
+    // } else {
+    //   console.log("Setting", key, "to", "*".repeat(inputFromPrompt.length));
+    // }
+
+    console.log(`Setting ${key} to ${inputFromPrompt}`);
+    const { exitCode, stderr } =
+      await $`${OPS} -config ${key}=${inputFromPrompt}`.nothrow();
+    if (exitCode !== 0) {
+      cancel(stderr.toString());
+      return process.exit(1);
+    }
+  }
+}
+
+export function findMissingConfig(
+  config: Record<string, any>,
+  opsCurrentConfig: string
+): Prompts {
+  let newConfig: Record<string, any> = {};
+
+  let opsConfigKeys = opsCurrentConfig.split("\n").map((line) => 
line.split("=")[0]);
+
+  for (const key in config) {
+    if (opsConfigKeys.includes(key)) {
+      continue;
+    }
+    newConfig[key] = config[key];
+  }
+
+  return newConfig;
+}
+
+export function readPositionalFile(positionals: string[]): {
+  success: boolean;
+  message?: string;
+  help?: string;
+  jsonFilePath?: string;
+} {
+
+  if (positionals.length < 2) {
+    console.error("This should not happen");
+    return { success: false, message: "This should not happen" };
+  }
+
+  if (positionals.length === 2) {
+    return { success: true, help: HelpMsg };
+  }
+
+  if (positionals.length > 3) {
+    return {
+      success: true,
+      message: AdditionalArgsMsg,
+      jsonFilePath: positionals[2],
+    };
+  }
+
+  return { success: true, jsonFilePath: positionals[2] };
+}
+
+export async function parsePositionalFile(
+  path: string
+): Promise<{ success: boolean; message?: string; body?: any }> {
+
+  const file = Bun.file(Bun.pathToFileURL(path));
+
+  if (!await file.exists()) {
+    return { success: false, message: FileNotFoundJsonMsg + 
Bun.pathToFileURL(path) };
+  }
+
+
+  try {
+    const contents = await file.json();
+    return { success: true, body: contents };
+  } catch (error) {
+    return { success: false, message: NotValidJsonMsg };
+  }
+}
+
+export function isInputConfigValid(body: Record<string, any>): boolean {
+  // 1. If the body is empty, return false
+  if (Object.keys(body).length === 0) {
+    return false;
+  }
+
+  // 2. Check that each key in the body has the keys as the Prompt type
+  for (const key in body) {
+    const value = body[key];
+    if (typeof value !== "object") {
+      return false;
+    }
+
+    if (!value.type) {
+      return false;
+    }
+
+    if (
+      !["string", "int", "float", "bool", "password"].includes(value.type) &&
+      !Array.isArray(value.type)
+    ) {
+      return false;
+    }
+  }
+
+  // 3. Check that all the keys are uppercase and can only have underscores 
not at the beginning or end
+  for (const key in body) {
+    if (!/^[A-Z][A-Z_]|[0-9]*[A-Z]|[0-9]$/.test(key)) {
+      return false;
+    }
+  }
+
+  return true;
+}
diff --git a/configurator/index.ts b/configurator/index.ts
new file mode 100644
index 0000000..18e0159
--- /dev/null
+++ b/configurator/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import main from "./configurator";
+
+main().catch(console.error);
diff --git a/configurator/package.json b/configurator/package.json
new file mode 100644
index 0000000..bbd833c
--- /dev/null
+++ b/configurator/package.json
@@ -0,0 +1,19 @@
+{
+  "name": "prompt",
+  "module": "index.ts",
+  "type": "module",
+  "scripts": {
+    "start": "bun index.ts",
+    "test": "bun test tests/index.test.ts",
+    "build": "bun build --target=bun index.ts > configurator.js && mv 
configurator.js ../configurator.js"
+  },
+  "devDependencies": {
+    "@types/bun": "latest"
+  },
+  "peerDependencies": {
+    "typescript": "^5.0.0"
+  },
+  "dependencies": {
+    "@clack/prompts": "^0.7.0"
+  }
+}
diff --git a/configurator/tests/fixtures/bad.json 
b/configurator/tests/fixtures/bad.json
new file mode 100644
index 0000000..ff30235
--- /dev/null
+++ b/configurator/tests/fixtures/bad.json
@@ -0,0 +1 @@
+}
\ No newline at end of file
diff --git a/configurator/tests/fixtures/multi-keys.json 
b/configurator/tests/fixtures/multi-keys.json
new file mode 100644
index 0000000..9ba80cf
--- /dev/null
+++ b/configurator/tests/fixtures/multi-keys.json
@@ -0,0 +1,22 @@
+{
+  "HELLO": {
+    "label": "Set a greeting",
+    "type": "string"
+  },
+  "TESTINT": {
+    "type": "int"
+  },
+  "TESTFLOAT": {
+    "label": "Set a float",
+    "type": "float"
+  },
+  "TESTBOOL": {
+    "type": "bool"
+  },
+  "TESTPASSWORD": {
+    "type": "password"
+  },
+  "TESTSELECT": {
+    "type": ["one", "two", "three", "four options"]
+  }
+}
diff --git a/configurator/tests/fixtures/not-json.txt 
b/configurator/tests/fixtures/not-json.txt
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/configurator/tests/fixtures/not-json.txt
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/configurator/tests/fixtures/valid.json 
b/configurator/tests/fixtures/valid.json
new file mode 100644
index 0000000..50a753e
--- /dev/null
+++ b/configurator/tests/fixtures/valid.json
@@ -0,0 +1,11 @@
+{
+  "HELLO": {
+    "type": "string"
+  },
+  "HELLO3": {
+    "type": "string"
+  },
+  "ANOTHER": {
+    "type": "string"
+  }
+}
diff --git a/configurator/tests/fixtures/with-defaults.json 
b/configurator/tests/fixtures/with-defaults.json
new file mode 100644
index 0000000..ff94d44
--- /dev/null
+++ b/configurator/tests/fixtures/with-defaults.json
@@ -0,0 +1,26 @@
+{
+  "HELLO": {
+    "type": "string",
+    "default": "world"
+  },
+  "HELLO3": {
+    "type": "int",
+    "default": 3
+  },
+  "A_FLOAT": {
+    "type": "float",
+    "default": 3.14
+  },
+  "A_BOOL": {
+    "type": "bool",
+    "default": false
+  },
+  "A_PASSWORD": {
+    "type": "password",
+    "default": "mypass"
+  },
+  "A_SELECT": {
+    "type": ["one", "two", "three", "four options"],
+    "default": "two"
+  }
+}
diff --git a/configurator/tests/index.test.ts b/configurator/tests/index.test.ts
new file mode 100644
index 0000000..20f6acd
--- /dev/null
+++ b/configurator/tests/index.test.ts
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { expect, test, describe } from "bun:test";
+import {
+  readPositionalFile,
+  parsePositionalFile,
+  NotValidJsonMsg,
+  HelpMsg,
+  AdditionalArgsMsg,
+  isInputConfigValid,
+  findMissingConfig,
+  FileNotFoundJsonMsg,
+} from "../configurator.ts";
+
+describe("findMissingConfig", () => {
+  test("config has some opsConfig key", async () => {
+    const config: any = { hello: "world", world: "hello" };
+    const opsConfig = "hello=world";
+    const newC: any = findMissingConfig(config, opsConfig);
+    expect(newC).toEqual({ world: "hello" });
+  });
+
+  test("config has no opsConfig key", async () => {
+    const config: any = { hello: "world", world: "hello" };
+    const opsConfig = "not=existing";
+    const newC: any = findMissingConfig(config, opsConfig);
+    expect(newC).toEqual({ hello: "world", world: "hello" });
+  });
+
+  test("opsConfig has no key", async () => {
+    const config: any = { hello: "world", world: "hello" };
+    const opsConfig = "";
+    const newC: any = findMissingConfig(config, opsConfig);
+    expect(newC).toEqual({ hello: "world", world: "hello" });
+  });
+
+  test("config and opsConfig are the same", async () => {
+    const config: any = { hello: "world", world: "hello" };
+    const opsConfig = "hello=world\nworld=hello";
+    const newC = findMissingConfig(config, opsConfig);
+    expect(newC).toEqual({});
+  });
+});
+
+describe("validateConfigJson", () => {
+  test("empty config", async () => {
+    const res = isInputConfigValid({});
+    expect(res).toBe(false);
+  });
+
+  test("wrong json", async () => {
+    const res = isInputConfigValid({
+      some: "key",
+      properties: { hello: { type: "string" } },
+    });
+    expect(res).toBe(false);
+  });
+
+  test("valid json", async () => {
+    const res = isInputConfigValid({
+      HELLO: { type: "string" },
+      HELLO3: { type: "string" },
+      HELLO33: { type: "string" },
+      HELLO_3: { type: "string" },
+      HELLO_33: { type: "string" },
+      HELLO_A_1_B_2: { type: "string" },
+      HELLO_A1B2: { type: "string" },
+      HELLO_INT: { type: "int" },
+      HELLO_FLOAT: { type: "float" },
+      HELLO_BOOL: { type: "bool" },
+      HELLOPASS: { type: "password" },
+      HELLOENUM: { type: ["a", "b", "c"] },
+      HELLOENUM3: { type: ["a", "b", "c"] },
+    });
+    expect(res).toBe(true);
+  });
+});
+
+describe("readPositionalFile", () => {
+  test("no positional arg provided", async () => {
+    const res = readPositionalFile(["bun", "index.ts"]);
+    expect(res.success).toBe(true);
+    expect(res.help).toBe(HelpMsg);
+  });
+
+  test("too many positional arg provided", async () => {
+    const res = readPositionalFile(["bun", "index.ts", "config.json", 
"extra"]);
+    expect(res.success).toBe(true);
+    expect(res.jsonFilePath).toBe("config.json");
+    expect(res.message).toBe(AdditionalArgsMsg);
+  });
+
+  test("positional arg provided", async () => {
+    const res = readPositionalFile(["bun", "index.ts", "config.json"]);
+    expect(res.success).toBe(true);
+    expect(res.jsonFilePath).toBe("config.json");
+  });
+});
+
+describe("parsePositionalFile", () => {
+  test("file does not exist", async () => {
+    const filename = "not-existing.json";
+    const jsonRes = await parsePositionalFile(filename);
+    expect(jsonRes.success).toBe(false);
+    expect(jsonRes.message).toBe(FileNotFoundJsonMsg + 
Bun.pathToFileURL(filename));
+  });
+
+  test("not a json file", async () => {
+    const jsonRes = await parsePositionalFile("tests/fixtures/not-json.txt");
+    expect(jsonRes.success).toBe(false);
+    expect(jsonRes.message).toBe(NotValidJsonMsg);
+  });
+
+  test("not a valid json", async () => {
+    const jsonRes = await parsePositionalFile("tests/fixtures/bad.json");
+    expect(jsonRes.success).toBe(false);
+    expect(jsonRes.message).toBe(NotValidJsonMsg);
+  });
+
+  test("valid json", async () => {
+    const jsonRes = await parsePositionalFile("tests//fixtures/valid.json");
+    expect(jsonRes.success).toBe(true);
+    expect(jsonRes.body).toHaveProperty("HELLO", { type: "string" }  );
+    expect(jsonRes.body).toHaveProperty("HELLO3", { type: "string" } );
+    expect(jsonRes.body).toHaveProperty("HELLO_3", { type: "string" });
+  });
+});
diff --git a/configurator/tsconfig.json b/configurator/tsconfig.json
new file mode 100644
index 0000000..238655f
--- /dev/null
+++ b/configurator/tsconfig.json
@@ -0,0 +1,27 @@
+{
+  "compilerOptions": {
+    // Enable latest features
+    "lib": ["ESNext", "DOM"],
+    "target": "ESNext",
+    "module": "ESNext",
+    "moduleDetection": "force",
+    "jsx": "react-jsx",
+    "allowJs": true,
+
+    // Bundler mode
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "noEmit": true,
+
+    // Best practices
+    "strict": true,
+    "skipLibCheck": true,
+    "noFallthroughCasesInSwitch": true,
+
+    // Some stricter flags (disabled by default)
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noPropertyAccessFromIndexSignature": false
+  }
+}

Reply via email to