@procedures/json 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +41 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/csv.d.ts +57 -0
- package/dist/csv.d.ts.map +1 -0
- package/dist/csv.js +176 -0
- package/dist/csv.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +50 -0
- package/dist/module.js.map +1 -1
- package/dist/procedures/csv/index.d.ts +3 -0
- package/dist/procedures/csv/index.d.ts.map +1 -0
- package/dist/procedures/csv/index.js +3 -0
- package/dist/procedures/csv/index.js.map +1 -0
- package/dist/procedures/csv/parse.d.ts +12 -0
- package/dist/procedures/csv/parse.d.ts.map +1 -0
- package/dist/procedures/csv/parse.js +21 -0
- package/dist/procedures/csv/parse.js.map +1 -0
- package/dist/procedures/csv/stringify.d.ts +12 -0
- package/dist/procedures/csv/stringify.d.ts.map +1 -0
- package/dist/procedures/csv/stringify.js +23 -0
- package/dist/procedures/csv/stringify.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/value.d.ts +3 -1
- package/dist/value.d.ts.map +1 -1
- package/dist/value.js.map +1 -1
- package/package.json +5 -4
- package/src/csv.ts +216 -0
- package/src/index.ts +2 -0
- package/src/module.ts +61 -0
- package/src/procedures/csv/index.ts +2 -0
- package/src/procedures/csv/parse.ts +23 -0
- package/src/procedures/csv/stringify.ts +25 -0
- package/src/types.ts +42 -0
- package/src/value.ts +3 -1
package/dist/value.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"value.js","sourceRoot":"","sources":["../src/value.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,kGAAkG;AAClG,MAAM,CAAC,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"value.js","sourceRoot":"","sources":["../src/value.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@procedures/json",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "Pure JSON and
|
|
4
|
+
"version": "0.6.0",
|
|
5
|
+
"description": "Pure JSON, YAML and CSV procedures: a query with JSON Pointer or a path, JSON Patch, a diff, a merge patch, a JSON Schema check, YAML to JSON and back, and CSV (RFC 4180) to rows and back.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"procedures",
|
|
8
8
|
"json",
|
|
9
9
|
"yaml",
|
|
10
|
+
"csv",
|
|
10
11
|
"json-pointer",
|
|
11
12
|
"json-patch",
|
|
12
13
|
"merge-patch",
|
|
@@ -69,8 +70,8 @@
|
|
|
69
70
|
"procedures": "./dist/register.js"
|
|
70
71
|
},
|
|
71
72
|
"dependencies": {
|
|
72
|
-
"@procedures/client": "0.
|
|
73
|
-
"@procedures/compiler": "0.
|
|
73
|
+
"@procedures/client": "0.6.0",
|
|
74
|
+
"@procedures/compiler": "0.6.0",
|
|
74
75
|
"yaml": "^2.9.1",
|
|
75
76
|
"zod": "^4.6.5"
|
|
76
77
|
},
|
package/src/csv.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A CSV reader and writer (RFC 4180), without dependencies.
|
|
3
|
+
*
|
|
4
|
+
* - A field in double quotes can have the delimiter, a line break and a doubled quote (`""`).
|
|
5
|
+
* - A line break is CRLF, LF or CR. A line break at the end of the text does not start a row.
|
|
6
|
+
* - The parse removes a byte order mark at the start of the text.
|
|
7
|
+
* - A quote in a field without quotes is a normal character, as in most readers.
|
|
8
|
+
* - Each object that the parse makes has own data properties only, so a column `__proto__` stays data.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { hasOwn, isPlainObject, JsonError, put } from "./value.js";
|
|
12
|
+
|
|
13
|
+
/** A row of `parseCsv` with `header`: the fields by the names of the columns. */
|
|
14
|
+
export type CsvRecord = Record<string, string>;
|
|
15
|
+
|
|
16
|
+
/** A value that `stringifyCsv` writes as a field. Null and undefined give an empty field. */
|
|
17
|
+
export type CsvCell = string | number | boolean | null | undefined;
|
|
18
|
+
|
|
19
|
+
/** The options of `parseCsv`. */
|
|
20
|
+
export interface CsvParseOptions {
|
|
21
|
+
/** The character between two fields. The default is a comma. */
|
|
22
|
+
readonly delimiter?: string | undefined;
|
|
23
|
+
/** True: the first row has the names of the columns, and each other row becomes an object. The default is false. */
|
|
24
|
+
readonly header?: boolean | undefined;
|
|
25
|
+
/** True: the parse removes the white space at the start and at the end of each field. The default is false. */
|
|
26
|
+
readonly trim?: boolean | undefined;
|
|
27
|
+
/** False: an empty line gives a row with one empty field. The default (true) skips each empty line. */
|
|
28
|
+
readonly skipEmptyLines?: boolean | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The options of `stringifyCsv`. */
|
|
32
|
+
export interface CsvStringifyOptions {
|
|
33
|
+
/** The character between two fields. The default is a comma. */
|
|
34
|
+
readonly delimiter?: string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* The names of the columns. For rows that are objects, the names select the keys and give their
|
|
37
|
+
* order. For rows that are arrays, the names are the first row.
|
|
38
|
+
*/
|
|
39
|
+
readonly columns?: readonly string[] | undefined;
|
|
40
|
+
/** False: the text has no row with the names of the columns. The default is true. */
|
|
41
|
+
readonly header?: boolean | undefined;
|
|
42
|
+
/** The line break after each row: `"\r\n"` (the default, as RFC 4180 tells) or `"\n"`. */
|
|
43
|
+
readonly lineBreak?: "\r\n" | "\n" | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The byte order mark. */
|
|
47
|
+
const BOM = "";
|
|
48
|
+
|
|
49
|
+
/** One row of the text, before the header. */
|
|
50
|
+
interface RawRow {
|
|
51
|
+
readonly fields: string[];
|
|
52
|
+
/** The line where the row starts, from 1. */
|
|
53
|
+
readonly line: number;
|
|
54
|
+
/** True for an empty line: one field without quotes and without text. */
|
|
55
|
+
readonly empty: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The delimiter of the options. It is one character, and not a quote or a line break. */
|
|
59
|
+
function delimiterOf(delimiter: string | undefined, label: string): string {
|
|
60
|
+
if (delimiter === undefined) return ",";
|
|
61
|
+
if (delimiter.length !== 1 || delimiter === '"' || delimiter === "\r" || delimiter === "\n") {
|
|
62
|
+
throw new JsonError("INVALID_CSV", `${label}: the delimiter ${JSON.stringify(delimiter)} must be one character, and not a quote or a line break`);
|
|
63
|
+
}
|
|
64
|
+
return delimiter;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The number of line breaks in a text. CRLF is one line break. */
|
|
68
|
+
const lineBreaks = (text: string): number => text.match(/\r\n|\r|\n/g)?.length ?? 0;
|
|
69
|
+
|
|
70
|
+
/** True for a space or a tab that is not the delimiter. */
|
|
71
|
+
const isBlank = (char: string | undefined, delimiter: string): boolean => (char === " " || char === "\t") && char !== delimiter;
|
|
72
|
+
|
|
73
|
+
/** This function reads the rows of a CSV text. */
|
|
74
|
+
function readRows(text: string, delimiter: string, trim: boolean): RawRow[] {
|
|
75
|
+
const rows: RawRow[] = [];
|
|
76
|
+
const end = text.length;
|
|
77
|
+
let at = 0;
|
|
78
|
+
let line = 1;
|
|
79
|
+
while (at < end) {
|
|
80
|
+
const start = line;
|
|
81
|
+
const fields: string[] = [];
|
|
82
|
+
let quoted = false;
|
|
83
|
+
for (;;) {
|
|
84
|
+
let next = at;
|
|
85
|
+
if (trim) while (isBlank(text[next], delimiter)) next += 1;
|
|
86
|
+
if (text[next] === '"') {
|
|
87
|
+
// A field in quotes: it ends at a quote that is not doubled
|
|
88
|
+
const open = line;
|
|
89
|
+
let value = "";
|
|
90
|
+
let from = next + 1;
|
|
91
|
+
for (;;) {
|
|
92
|
+
const quote = text.indexOf('"', from);
|
|
93
|
+
if (quote === -1) throw new JsonError("INVALID_CSV", `csv.parse: the quote at line ${open} has no closing quote`);
|
|
94
|
+
value += text.slice(from, quote);
|
|
95
|
+
if (text[quote + 1] === '"') {
|
|
96
|
+
value += '"';
|
|
97
|
+
from = quote + 2;
|
|
98
|
+
} else {
|
|
99
|
+
from = quote + 1;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
line += lineBreaks(text.slice(next, from));
|
|
104
|
+
if (trim) while (isBlank(text[from], delimiter)) from += 1;
|
|
105
|
+
const after = text[from];
|
|
106
|
+
if (after !== undefined && after !== delimiter && after !== "\r" && after !== "\n") {
|
|
107
|
+
throw new JsonError("INVALID_CSV", `csv.parse: line ${line} has the character ${JSON.stringify(after)} after the closing quote of a field`);
|
|
108
|
+
}
|
|
109
|
+
fields.push(trim ? value.trim() : value);
|
|
110
|
+
quoted = true;
|
|
111
|
+
at = from;
|
|
112
|
+
} else {
|
|
113
|
+
let stop = at;
|
|
114
|
+
while (stop < end && text[stop] !== delimiter && text[stop] !== "\r" && text[stop] !== "\n") stop += 1;
|
|
115
|
+
const value = text.slice(at, stop);
|
|
116
|
+
fields.push(trim ? value.trim() : value);
|
|
117
|
+
at = stop;
|
|
118
|
+
}
|
|
119
|
+
if (text[at] !== delimiter) break;
|
|
120
|
+
at += 1;
|
|
121
|
+
}
|
|
122
|
+
if (text[at] === "\r" || text[at] === "\n") {
|
|
123
|
+
at += text[at] === "\r" && text[at + 1] === "\n" ? 2 : 1;
|
|
124
|
+
line += 1;
|
|
125
|
+
}
|
|
126
|
+
rows.push({ fields, line: start, empty: fields.length === 1 && !quoted && fields[0] === "" });
|
|
127
|
+
}
|
|
128
|
+
return rows;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** This function makes the objects of the rows with the first row as the names of the columns. */
|
|
132
|
+
function records(rows: readonly RawRow[]): CsvRecord[] {
|
|
133
|
+
const [head, ...body] = rows;
|
|
134
|
+
if (head === undefined) return [];
|
|
135
|
+
const names = head.fields;
|
|
136
|
+
const seen = new Set<string>();
|
|
137
|
+
for (const name of names) {
|
|
138
|
+
if (seen.has(name)) throw new JsonError("INVALID_CSV", `csv.parse: the header has the column ${JSON.stringify(name)} two times`);
|
|
139
|
+
seen.add(name);
|
|
140
|
+
}
|
|
141
|
+
return body.map((row) => {
|
|
142
|
+
if (row.fields.length > names.length) {
|
|
143
|
+
throw new JsonError("INVALID_CSV", `csv.parse: the row at line ${row.line} has ${row.fields.length} fields, and the header has ${names.length}`);
|
|
144
|
+
}
|
|
145
|
+
const record: CsvRecord = {};
|
|
146
|
+
names.forEach((name, index) => put(record, name, row.fields[index] ?? ""));
|
|
147
|
+
return record;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* This function parses CSV text (RFC 4180) into rows. Each row is an array of its fields. With
|
|
153
|
+
* `header`, the first row gives the names of the columns, and each other row is an object. A
|
|
154
|
+
* row with fewer fields gets empty text for the missing columns. Bad text throws `INVALID_CSV`.
|
|
155
|
+
*/
|
|
156
|
+
export function parseCsv(text: string, options: CsvParseOptions & { readonly header: true }): CsvRecord[];
|
|
157
|
+
export function parseCsv(text: string, options?: CsvParseOptions & { readonly header?: false | undefined }): string[][];
|
|
158
|
+
export function parseCsv(text: string, options?: CsvParseOptions): string[][] | CsvRecord[];
|
|
159
|
+
export function parseCsv(text: string, options: CsvParseOptions = {}): string[][] | CsvRecord[] {
|
|
160
|
+
const delimiter = delimiterOf(options.delimiter, "csv.parse");
|
|
161
|
+
const body = text.startsWith(BOM) ? text.slice(BOM.length) : text;
|
|
162
|
+
const skip = options.skipEmptyLines ?? true;
|
|
163
|
+
const rows = readRows(body, delimiter, options.trim ?? false).filter((row) => !(skip && row.empty));
|
|
164
|
+
return options.header ? records(rows) : rows.map((row) => row.fields);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The text of one field. */
|
|
168
|
+
function cellText(value: unknown, where: string): string {
|
|
169
|
+
if (value === null || value === undefined) return "";
|
|
170
|
+
if (typeof value === "string") return value;
|
|
171
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
172
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
173
|
+
throw new JsonError("INVALID_CSV", `csv.stringify: ${where} is not a string, a finite number, a boolean or null`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The CSV form of one row. A field gets quotes when it has the delimiter, a quote or a line break. */
|
|
177
|
+
function rowText(cells: readonly string[], delimiter: string): string {
|
|
178
|
+
// A row of one empty field gets quotes: an empty line is not a row for the parse
|
|
179
|
+
if (cells.length === 1 && cells[0] === "") return '""';
|
|
180
|
+
return cells
|
|
181
|
+
.map((cell) => (cell.includes(delimiter) || cell.includes('"') || cell.includes("\r") || cell.includes("\n") ? `"${cell.replaceAll('"', '""')}"` : cell))
|
|
182
|
+
.join(delimiter);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* This function writes rows as CSV text (RFC 4180). The rows are all arrays or all objects. For
|
|
187
|
+
* objects, the first row has the names of the columns: by default, each key in the order that
|
|
188
|
+
* the rows first give it. Each row ends with a line break. Bad rows throw `INVALID_CSV`.
|
|
189
|
+
*/
|
|
190
|
+
export function stringifyCsv(rows: readonly (readonly CsvCell[])[] | readonly Readonly<Record<string, CsvCell>>[], options: CsvStringifyOptions = {}): string {
|
|
191
|
+
const delimiter = delimiterOf(options.delimiter, "csv.stringify");
|
|
192
|
+
const lineBreak = options.lineBreak ?? "\r\n";
|
|
193
|
+
if (lineBreak !== "\r\n" && lineBreak !== "\n") throw new JsonError("INVALID_CSV", 'csv.stringify: the line break must be "\\r\\n" or "\\n"');
|
|
194
|
+
const list: readonly unknown[] = rows;
|
|
195
|
+
const arrays = list.every((row) => Array.isArray(row));
|
|
196
|
+
if (!arrays && !list.every((row) => isPlainObject(row))) {
|
|
197
|
+
throw new JsonError("INVALID_CSV", "csv.stringify: the rows must be all arrays or all objects");
|
|
198
|
+
}
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
let columns = options.columns;
|
|
201
|
+
if (!arrays && columns === undefined) {
|
|
202
|
+
const names = new Set<string>();
|
|
203
|
+
for (const row of list as readonly Record<string, unknown>[]) for (const key of Object.keys(row)) names.add(key);
|
|
204
|
+
columns = [...names];
|
|
205
|
+
}
|
|
206
|
+
if (columns !== undefined && options.header !== false && columns.length > 0) {
|
|
207
|
+
lines.push(rowText(columns.map((name, index) => cellText(name, `column ${index + 1} of the header`)), delimiter));
|
|
208
|
+
}
|
|
209
|
+
list.forEach((row, index) => {
|
|
210
|
+
const cells = arrays
|
|
211
|
+
? (row as readonly unknown[]).map((value, column) => cellText(value, `field ${column + 1} of row ${index + 1}`))
|
|
212
|
+
: (columns ?? []).map((name) => cellText(hasOwn(row as object, name) ? (row as Record<string, unknown>)[name] : undefined, `the field ${JSON.stringify(name)} of row ${index + 1}`));
|
|
213
|
+
lines.push(rowText(cells, delimiter));
|
|
214
|
+
});
|
|
215
|
+
return lines.map((text) => text + lineBreak).join("");
|
|
216
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./module.js";
|
|
|
3
3
|
export * from "./types.js";
|
|
4
4
|
export { jsonQuery, jsonPatch, jsonDiff, jsonMerge, jsonValidate } from "./procedures/json/index.js";
|
|
5
5
|
export { yamlParse, yamlStringify } from "./procedures/yaml/index.js";
|
|
6
|
+
export { csvParse, csvStringify } from "./procedures/csv/index.js";
|
|
6
7
|
// The functions behind the procedures, for a program in the same process
|
|
7
8
|
export { assertJson, cloneJson, jsonEqual, JsonError, PROTOTYPE_KEYS, formatPointer, escapeToken, type JsonErrorCode } from "./value.js";
|
|
8
9
|
export { parsePointer, lookup, arrayIndex, type Lookup } from "./pointer.js";
|
|
@@ -12,3 +13,4 @@ export { diffJson } from "./diff.js";
|
|
|
12
13
|
export { mergePatch } from "./merge.js";
|
|
13
14
|
export { validateJson, NOT_EXAMINED, type ValidationIssue, type ValidationResult } from "./schema.js";
|
|
14
15
|
export { parseYaml, stringifyYaml, type YamlParseOptions, type YamlStringifyOptions } from "./yaml.js";
|
|
16
|
+
export { parseCsv, stringifyCsv, type CsvCell, type CsvParseOptions, type CsvRecord, type CsvStringifyOptions } from "./csv.js";
|
package/src/module.ts
CHANGED
|
@@ -16,7 +16,13 @@ import { jsonMerge } from "./procedures/json/merge.js";
|
|
|
16
16
|
import { jsonValidate } from "./procedures/json/validate.js";
|
|
17
17
|
import { yamlParse } from "./procedures/yaml/parse.js";
|
|
18
18
|
import { yamlStringify } from "./procedures/yaml/stringify.js";
|
|
19
|
+
import { csvParse } from "./procedures/csv/parse.js";
|
|
20
|
+
import { csvStringify } from "./procedures/csv/stringify.js";
|
|
19
21
|
import type {
|
|
22
|
+
CsvParseInput,
|
|
23
|
+
CsvParseOutput,
|
|
24
|
+
CsvStringifyInput,
|
|
25
|
+
CsvStringifyOutput,
|
|
20
26
|
JsonDiffInput,
|
|
21
27
|
JsonDiffOutput,
|
|
22
28
|
JsonMergeInput,
|
|
@@ -109,6 +115,31 @@ const YamlStringifyInputSchema = z.object({
|
|
|
109
115
|
|
|
110
116
|
const YamlStringifyOutputSchema = z.object({ text: z.string().describe("The YAML text") });
|
|
111
117
|
|
|
118
|
+
const delimiter = z.string().length(1).optional().describe("The character between two fields (default: a comma)");
|
|
119
|
+
|
|
120
|
+
const CsvParseInputSchema = z.object({
|
|
121
|
+
text: z.string().describe("The CSV text (RFC 4180)"),
|
|
122
|
+
header: z.boolean().optional().describe("True: the first row has the names of the columns, and each other row is an object"),
|
|
123
|
+
delimiter,
|
|
124
|
+
trim: z.boolean().optional().describe("True: remove the white space at the start and at the end of each field"),
|
|
125
|
+
skipEmptyLines: z.boolean().optional().describe("False: an empty line gives a row with one empty field (default: true, skip each empty line)"),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// The schema does not copy the rows, so a column `__proto__` stays data. The handler gives the type of the rows
|
|
129
|
+
const CsvParseOutputSchema = z.object({
|
|
130
|
+
rows: z.array(json("A row: an array of fields, or with header an object of fields by column name")).describe("The rows, in the order of the text"),
|
|
131
|
+
}) as z.ZodType<CsvParseOutput>;
|
|
132
|
+
|
|
133
|
+
const CsvStringifyInputSchema = z.object({
|
|
134
|
+
rows: z.array(json("A row: an array of fields, or an object of fields by column name")).describe("The rows: all arrays or all objects. A field is a string, a number, a boolean or null"),
|
|
135
|
+
columns: z.array(z.string()).optional().describe("The names of the columns. For objects, they select the keys and give their order. For arrays, they are the first row"),
|
|
136
|
+
header: z.boolean().optional().describe("False: no row with the names of the columns (default: true)"),
|
|
137
|
+
delimiter,
|
|
138
|
+
lineBreak: z.enum(["\r\n", "\n"]).optional().describe("The line break after each row (default: CRLF)"),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const CsvStringifyOutputSchema = z.object({ text: z.string().describe("The CSV text") });
|
|
142
|
+
|
|
112
143
|
// =============================================================================
|
|
113
144
|
// Procedures
|
|
114
145
|
// =============================================================================
|
|
@@ -211,6 +242,34 @@ const yamlStringifyProcedure = createProcedure()
|
|
|
211
242
|
.handler(async (input: YamlStringifyInput): Promise<YamlStringifyOutput> => yamlStringify(input))
|
|
212
243
|
.build();
|
|
213
244
|
|
|
245
|
+
const csvParseProcedure = createProcedure()
|
|
246
|
+
.path(["csv", "parse"])
|
|
247
|
+
.input(zodAdapter<CsvParseInput>(CsvParseInputSchema))
|
|
248
|
+
.output(zodAdapter<CsvParseOutput>(CsvParseOutputSchema))
|
|
249
|
+
.meta({
|
|
250
|
+
description: "Parse CSV text into rows",
|
|
251
|
+
args: ["text"],
|
|
252
|
+
shorts: { delimiter: "d", trim: "t" },
|
|
253
|
+
output: "json",
|
|
254
|
+
effects: PURE_EFFECTS,
|
|
255
|
+
})
|
|
256
|
+
.handler(async (input: CsvParseInput): Promise<CsvParseOutput> => csvParse(input))
|
|
257
|
+
.build();
|
|
258
|
+
|
|
259
|
+
const csvStringifyProcedure = createProcedure()
|
|
260
|
+
.path(["csv", "stringify"])
|
|
261
|
+
.input(zodAdapter<CsvStringifyInput>(CsvStringifyInputSchema))
|
|
262
|
+
.output(zodAdapter<CsvStringifyOutput>(CsvStringifyOutputSchema))
|
|
263
|
+
.meta({
|
|
264
|
+
description: "Write rows as CSV text",
|
|
265
|
+
args: [],
|
|
266
|
+
shorts: { delimiter: "d" },
|
|
267
|
+
output: "json",
|
|
268
|
+
effects: PURE_EFFECTS,
|
|
269
|
+
})
|
|
270
|
+
.handler(async (input: CsvStringifyInput): Promise<CsvStringifyOutput> => csvStringify(input))
|
|
271
|
+
.build();
|
|
272
|
+
|
|
214
273
|
/**
|
|
215
274
|
* The module of this package, for `createRegistry()`: its procedures as data. Importing this
|
|
216
275
|
* file registers nothing.
|
|
@@ -223,6 +282,8 @@ export const jsonModule: ProcedureModule = defineModule("@procedures/json", [
|
|
|
223
282
|
jsonValidateProcedure,
|
|
224
283
|
yamlParseProcedure,
|
|
225
284
|
yamlStringifyProcedure,
|
|
285
|
+
csvParseProcedure,
|
|
286
|
+
csvStringifyProcedure,
|
|
226
287
|
]);
|
|
227
288
|
|
|
228
289
|
export default jsonModule;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* csv.parse procedure
|
|
3
|
+
*
|
|
4
|
+
* Parse CSV text (RFC 4180) into rows. With `header`, the first row gives the names of the
|
|
5
|
+
* columns, and each other row is an object.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parseCsv } from "../../csv.js";
|
|
9
|
+
import type { CsvParseInput, CsvParseOutput } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse CSV text into rows
|
|
13
|
+
*/
|
|
14
|
+
export async function csvParse(input: CsvParseInput): Promise<CsvParseOutput> {
|
|
15
|
+
return {
|
|
16
|
+
rows: parseCsv(input.text, {
|
|
17
|
+
header: input.header,
|
|
18
|
+
delimiter: input.delimiter,
|
|
19
|
+
trim: input.trim,
|
|
20
|
+
skipEmptyLines: input.skipEmptyLines,
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* csv.stringify procedure
|
|
3
|
+
*
|
|
4
|
+
* Write rows as CSV text (RFC 4180). The rows are all arrays or all objects. `csv.parse` of the
|
|
5
|
+
* text gives the rows again.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { stringifyCsv, type CsvCell } from "../../csv.js";
|
|
9
|
+
import type { CsvStringifyInput, CsvStringifyOutput } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Write rows as CSV text
|
|
13
|
+
*/
|
|
14
|
+
export async function csvStringify(input: CsvStringifyInput): Promise<CsvStringifyOutput> {
|
|
15
|
+
// stringifyCsv examines each row and each field
|
|
16
|
+
const rows = input.rows as readonly (readonly CsvCell[])[];
|
|
17
|
+
return {
|
|
18
|
+
text: stringifyCsv(rows, {
|
|
19
|
+
delimiter: input.delimiter,
|
|
20
|
+
columns: input.columns,
|
|
21
|
+
header: input.header,
|
|
22
|
+
lineBreak: input.lineBreak,
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -125,3 +125,45 @@ export interface YamlStringifyInput {
|
|
|
125
125
|
export interface YamlStringifyOutput {
|
|
126
126
|
text: string;
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// csv.parse Types - Parse CSV text into rows
|
|
131
|
+
// =============================================================================
|
|
132
|
+
|
|
133
|
+
export interface CsvParseInput {
|
|
134
|
+
text: string;
|
|
135
|
+
/** True: the first row has the names of the columns, and each other row is an object. */
|
|
136
|
+
header?: boolean | undefined;
|
|
137
|
+
/** The character between two fields. The default is a comma. */
|
|
138
|
+
delimiter?: string | undefined;
|
|
139
|
+
/** True: the parse removes the white space at the start and at the end of each field. */
|
|
140
|
+
trim?: boolean | undefined;
|
|
141
|
+
/** False: an empty line gives a row with one empty field. The default skips each empty line. */
|
|
142
|
+
skipEmptyLines?: boolean | undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface CsvParseOutput {
|
|
146
|
+
/** The rows: arrays of fields, or with `header`, objects of fields by the names of the columns. */
|
|
147
|
+
rows: string[][] | Record<string, string>[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// csv.stringify Types - Write rows as CSV text
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
export interface CsvStringifyInput {
|
|
155
|
+
/** The rows: all arrays of fields, or all objects of fields by the names of the columns. */
|
|
156
|
+
rows: unknown[];
|
|
157
|
+
/** The names of the columns. For objects, they select the keys and give their order. For arrays, they are the first row. */
|
|
158
|
+
columns?: string[] | undefined;
|
|
159
|
+
/** False: the text has no row with the names of the columns. */
|
|
160
|
+
header?: boolean | undefined;
|
|
161
|
+
/** The character between two fields. The default is a comma. */
|
|
162
|
+
delimiter?: string | undefined;
|
|
163
|
+
/** The line break after each row. The default is CRLF. */
|
|
164
|
+
lineBreak?: "\r\n" | "\n" | undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface CsvStringifyOutput {
|
|
168
|
+
text: string;
|
|
169
|
+
}
|
package/src/value.ts
CHANGED
|
@@ -27,7 +27,9 @@ export type JsonErrorCode =
|
|
|
27
27
|
/** The YAML text is not valid, or it has a value that JSON does not have. */
|
|
28
28
|
| "INVALID_YAML"
|
|
29
29
|
/** The JSON Schema is not a schema. */
|
|
30
|
-
| "INVALID_SCHEMA"
|
|
30
|
+
| "INVALID_SCHEMA"
|
|
31
|
+
/** The CSV text is not valid, or a row or an option of the CSV writer is not valid. */
|
|
32
|
+
| "INVALID_CSV";
|
|
31
33
|
|
|
32
34
|
/** An error of a procedure of this package. A server sends it with its own code, for example `NOT_JSON`. */
|
|
33
35
|
export class JsonError extends Error {
|