forked from mxx1111/sparepack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfixtures.mjs
More file actions
124 lines (114 loc) · 4.75 KB
/
Copy pathfixtures.mjs
File metadata and controls
124 lines (114 loc) · 4.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Fixture generation: keep the shape, drop the data.
//
// A worker needs to know that `orders.json` is an array of objects with `id`, `total`,
// and a nested `customer` — they do not need your customers. These generators read the
// real file to learn its structure and emit a synthetic file with the same structure.
//
// Everything here is deterministic. A pack rebuilt from the same inputs produces byte-identical
// output, so a diff between two packs means something changed rather than that a PRNG moved.
const DEFAULT_ARRAY_SAMPLES = 3
export class FixtureError extends Error {}
/** Key-name heuristics, so a synthetic record still reads like the thing it stands for. */
function syntheticString(key, index) {
const k = key.toLowerCase()
if (/mail/.test(k)) return `user${index}@example.com`
if (/(^|_)(id|uuid|guid)$/.test(k)) return `id-${String(index).padStart(4, '0')}`
if (/(phone|mobile|tel)/.test(k)) return '000-0000-0000'
if (/(url|link|href|endpoint)/.test(k)) return 'https://example.com/path'
// `At$` is tested against the original key: lowercasing first would lose the camelCase
// boundary that separates `createdAt` from words merely ending in "at" like `format`.
if (/(date|time)/.test(k) || /(^|_)at$/.test(k) || /At$/.test(key)) return '2026-01-01T00:00:00Z'
if (/(name|title|label)/.test(k)) return `sample ${key} ${index}`
if (/(addr|street|city)/.test(k)) return ' 1 Example Street'
if (/(token|secret|key|password|credential)/.test(k)) return 'REDACTED-BY-SPAREPACK'
return `sample-${key || 'value'}-${index}`
}
function syntheticValue(value, key, index, samples) {
if (value === null) return null
if (Array.isArray(value)) {
if (!value.length) return []
const template = value[0]
return Array.from({ length: Math.min(samples, Math.max(value.length, 1)) }, (_, i) =>
syntheticValue(template, key, i + 1, samples),
)
}
switch (typeof value) {
case 'string':
return syntheticString(key, index)
case 'number':
return Number.isInteger(value) ? index : Number((index + 0.5).toFixed(2))
case 'boolean':
return false
case 'object': {
const out = {}
for (const [k, v] of Object.entries(value)) out[k] = syntheticValue(v, k, index, samples)
return out
}
default:
return null
}
}
function shapeFromJson(text, samples, path) {
let parsed
try {
parsed = JSON.parse(text)
} catch (err) {
throw new FixtureError(`fixtures["${path}"]: generator "shape" needs valid JSON — ${err.message}`)
}
return `${JSON.stringify(syntheticValue(parsed, '', 1, samples), null, 2)}\n`
}
/**
* Rebuild a delimited file: real header row, synthetic body.
* The header is a column contract; the rows are data.
*/
function rowsFromDelimited(text, count, path) {
const lines = text.split(/\r?\n/).filter((l) => l.trim())
if (!lines.length) throw new FixtureError(`fixtures["${path}"]: generator "rows" needs at least a header line`)
const delimiter = path.toLowerCase().endsWith('.tsv') ? '\t' : ','
const header = lines[0]
const columns = header.split(delimiter)
const body = Array.from({ length: count }, (_, i) =>
columns.map((col) => syntheticString(col.trim().replace(/^"|"$/g, ''), i + 1)).join(delimiter),
)
return `${[header, ...body].join('\n')}\n`
}
function parseSpec(spec, path) {
const match = /^([a-z]+)(?::(\d+))?$/i.exec(spec)
if (!match) {
throw new FixtureError(
`fixtures["${path}"]: unrecognised generator "${spec}". Use empty, shape[:n], rows:n, or text:n.`,
)
}
return { kind: match[1].toLowerCase(), count: match[2] ? Number(match[2]) : undefined }
}
/**
* @param spec generator string from sparepack.yaml
* @param source original file contents, or null when the file does not exist
* @returns generated file contents
*/
export function generateFixture(spec, source, path) {
const { kind, count } = parseSpec(spec, path)
const needsSource = kind === 'shape' || kind === 'rows'
if (needsSource && source === null) {
throw new FixtureError(
`fixtures["${path}"]: generator "${kind}" reads the real file to learn its structure, but ${path} does not exist`,
)
}
switch (kind) {
case 'empty':
return ''
case 'shape':
return shapeFromJson(source, count ?? DEFAULT_ARRAY_SAMPLES, path)
case 'rows':
if (count === undefined) throw new FixtureError(`fixtures["${path}"]: "rows" needs a count, e.g. rows:20`)
return rowsFromDelimited(source, count, path)
case 'text': {
const n = count ?? 5
return `${Array.from({ length: n }, (_, i) => `Placeholder line ${i + 1} generated by sparepack.`).join('\n')}\n`
}
default:
throw new FixtureError(
`fixtures["${path}"]: unknown generator "${kind}". Use empty, shape[:n], rows:n, or text:n.`,
)
}
}