diff --git a/package-lock.json b/package-lock.json index c1cce2e..1e3a079 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@slawek/sk-tools", - "version": "0.2.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@slawek/sk-tools", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "dependencies": { "d3-dsv": "^3.0.1", diff --git a/package.json b/package.json index 0d0f693..e38de7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@slawek/sk-tools", - "version": "0.2.1", + "version": "0.3.0", "type": "module", "files": [ "dist", diff --git a/src/cli.ts b/src/cli.ts index 1028168..04c3542 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,11 +3,10 @@ import { parseArgs } from "node:util"; -import { - renderCliOutput, - readCsvFromStdin, - readJsonFromStdin, -} from "./cli/utils.ts"; +import { runCommand } from "./cli/commands.ts"; +import { usageTableInfo } from "./cli/commands/table-info.ts"; +import { usageTable } from "./cli/commands/table.ts"; +import { renderCliOutput } from "./cli/utils.ts"; type CliValues = { help?: boolean; @@ -22,7 +21,8 @@ function usage(): string { return `Usage: sk-tools [options] Commands: - table Render stdin data as Markdown table + table, t Render stdin data as Markdown table + table-info, ti Print row/column stats and inferred column types Global options (all commands): --query, -q @@ -33,39 +33,19 @@ Use: sk-tools --help or: sk-tools --help`; } -function usageTable(): string { - return `Usage: sk-tools table [--from|-F ] [--columns|-C ] [global options] - -Options: - --from, -F Input format on stdin (default: json) - --columns, -C Column tokens: col (raw), col: (auto), col:Label (custom), with exact match via = prefix (e.g. =col:)`; -} - function usageCommand(command: string): string { switch (command) { case "table": + case "t": return usageTable(); + case "table-info": + case "ti": + return usageTableInfo(); default: return `Unknown command: ${command}\n\n${usage()}`; } } -async function runTableCommand(values: CliValues): Promise { - const from = (values.from ?? "json").toString().trim().toLowerCase(); - - if (from === "json") { - return readJsonFromStdin(); - } - if (from === "csv") { - return readCsvFromStdin(","); - } - if (from === "tsv") { - return readCsvFromStdin("\t"); - } - - throw new Error(`Invalid --from '${values.from}'. Allowed: json, csv, tsv`); -} - async function main(): Promise { const argv = process.argv.slice(2); const command = argv[0]; @@ -101,11 +81,12 @@ async function main(): Promise { process.exit(0); } - if (command !== "table") { - throw new Error(`Unknown command: ${command}`); + const output = await runCommand(command, typedValues); + if (typeof output === "string") { + console.log(output); + return; } - const output = await runTableCommand(typedValues); renderCliOutput(output, typedValues.output, typedValues.query, typedValues.columns); } diff --git a/src/cli/commands.ts b/src/cli/commands.ts new file mode 100644 index 0000000..28873c5 --- /dev/null +++ b/src/cli/commands.ts @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +import { runTableCommand } from "./commands/table.ts"; +import { runTableInfoCommand } from "./commands/table-info.ts"; + +type CommandValues = { + [key: string]: string | boolean | undefined; +}; + +export async function runCommand(command: string, values: CommandValues): Promise { + switch (command) { + case "table": + case "t": + return runTableCommand(values); + case "table-info": + case "ti": + return runTableInfoCommand(values); + default: + throw new Error(`Unknown command: ${command}`); + } +} diff --git a/src/cli/commands/table-info.ts b/src/cli/commands/table-info.ts new file mode 100644 index 0000000..4f87912 --- /dev/null +++ b/src/cli/commands/table-info.ts @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT + +import { + readCsvFromStdin, + readJsonFromStdin, +} from "../utils.ts"; + +type TableInfoCommandValues = { + from?: string; + [key: string]: string | boolean | undefined; +}; + +type RowObject = Record; + +type ValueType = "string" | "number" | "boolean" | "object" | "array" | "null" | "unknown"; + +function getType(value: unknown): ValueType { + if (value === null) return "null"; + if (Array.isArray(value)) return "array"; + if (value === undefined) return "unknown"; + if (typeof value === "string") return "string"; + if (typeof value === "number") return "number"; + if (typeof value === "boolean") return "boolean"; + if (typeof value === "object") return "object"; + return "unknown"; +} + +function normalizeRows(input: unknown): { rowCount: number; rows: RowObject[] } { + if (Array.isArray(input)) { + return { + rowCount: input.length, + rows: input.map((item) => + item && typeof item === "object" && !Array.isArray(item) + ? item as RowObject + : { value: item } + ), + }; + } + + if (input && typeof input === "object") { + return { + rowCount: 1, + rows: [input as RowObject], + }; + } + + return { + rowCount: 1, + rows: [{ value: input }], + }; +} + +function inferColumnType(rows: RowObject[], column: string): string { + const kinds = new Set(); + + for (const row of rows) { + if (!(column in row)) { + continue; + } + + const kind = getType(row[column]); + if (kind !== "unknown") { + kinds.add(kind); + } + } + + if (kinds.size === 0) { + return "unknown"; + } + + if (kinds.size === 1) { + return [...kinds][0]; + } + + if (kinds.size === 2 && kinds.has("null")) { + return [...kinds].find((kind) => kind !== "null") ?? "unknown"; + } + + return `mixed(${[...kinds].join("|")})`; +} + +function buildInfo(input: unknown): string { + const { rowCount, rows } = normalizeRows(input); + const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))]; + + const lines = [ + `Number of columns: ${columns.length}`, + `Number of rows: ${rowCount}`, + "", + "Columns:", + ]; + + if (columns.length === 0) { + lines.push("- (none)"); + return lines.join("\n"); + } + + for (const column of columns) { + lines.push(`- ${column}: ${inferColumnType(rows, column)}`); + } + + return lines.join("\n"); +} + +export function usageTableInfo(): string { + return `Usage: sk-tools table-info [--from|-F ] + +Options: + --from, -F Input format on stdin (default: json)`; +} + +export async function runTableInfoCommand(values: TableInfoCommandValues): Promise { + const from = (values.from ?? "json").toString().trim().toLowerCase(); + + if (from === "json") { + return buildInfo(await readJsonFromStdin()); + } + if (from === "csv") { + return buildInfo(await readCsvFromStdin(",")); + } + if (from === "tsv") { + return buildInfo(await readCsvFromStdin("\t")); + } + + throw new Error(`Invalid --from '${values.from}'. Allowed: json, csv, tsv`); +} diff --git a/src/cli/commands/table.ts b/src/cli/commands/table.ts new file mode 100644 index 0000000..7476023 --- /dev/null +++ b/src/cli/commands/table.ts @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +import { + readCsvFromStdin, + readJsonFromStdin, +} from "../utils.ts"; + +type TableCommandValues = { + from?: string; + [key: string]: string | boolean | undefined; +}; + +export function usageTable(): string { + return `Usage: sk-tools table [--from|-F ] [--columns|-C ] [global options] + +Options: + --from, -F Input format on stdin (default: json) + --columns, -C Column tokens: col (raw), col: (auto), col:Label (custom), with exact match via = prefix (e.g. =col:)`; +} + +export async function runTableCommand(values: TableCommandValues): Promise { + const from = (values.from ?? "json").toString().trim().toLowerCase(); + + if (from === "json") { + return readJsonFromStdin(); + } + if (from === "csv") { + return readCsvFromStdin(","); + } + if (from === "tsv") { + return readCsvFromStdin("\t"); + } + + throw new Error(`Invalid --from '${values.from}'. Allowed: json, csv, tsv`); +} diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 344b836..10bc147 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -115,7 +115,7 @@ function isScalar(value: unknown): value is Scalar { return value == null || typeof value !== "object"; } -function getScalarRowsAndHeaders(value: unknown): { headers: string[]; rows: ScalarRow[] } { +export function getScalarRowsAndHeaders(value: unknown): { headers: string[]; rows: ScalarRow[] } { let rows: Array>; if (Array.isArray(value)) { rows = value.map((item) =>