@venn-lang/data 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/dist/index.d.ts +152 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +1941 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +54 -0
  8. package/src/actions/faker-actions.ts +22 -0
  9. package/src/actions/index.ts +7 -0
  10. package/src/actions/parse-actions.ts +31 -0
  11. package/src/actions/random-actions.ts +51 -0
  12. package/src/csv/csv.types.ts +2 -0
  13. package/src/csv/index.ts +2 -0
  14. package/src/csv/parse-csv.ts +30 -0
  15. package/src/faker/address.ts +70 -0
  16. package/src/faker/all-specs.ts +25 -0
  17. package/src/faker/brazil-documents.ts +46 -0
  18. package/src/faker/brazil.ts +75 -0
  19. package/src/faker/check-digits.ts +56 -0
  20. package/src/faker/commerce.ts +59 -0
  21. package/src/faker/company.ts +41 -0
  22. package/src/faker/data/business.ts +80 -0
  23. package/src/faker/data/commerce.ts +73 -0
  24. package/src/faker/data/index.ts +36 -0
  25. package/src/faker/data/names.ts +93 -0
  26. package/src/faker/data/places.ts +109 -0
  27. package/src/faker/data/web.ts +45 -0
  28. package/src/faker/data/words.ts +104 -0
  29. package/src/faker/datetime.ts +102 -0
  30. package/src/faker/faker.types.ts +24 -0
  31. package/src/faker/finance.ts +86 -0
  32. package/src/faker/ids.ts +80 -0
  33. package/src/faker/index.ts +8 -0
  34. package/src/faker/internet.ts +100 -0
  35. package/src/faker/person.ts +88 -0
  36. package/src/faker/primitives.ts +77 -0
  37. package/src/faker/text.ts +56 -0
  38. package/src/index.ts +8 -0
  39. package/src/plugin.ts +17 -0
  40. package/src/rng/index.ts +4 -0
  41. package/src/rng/mulberry32.ts +18 -0
  42. package/src/rng/rng.types.ts +2 -0
  43. package/src/rng/shared-rng.ts +15 -0
  44. package/src/rng/shuffle.ts +17 -0
  45. package/src/types.ts +14 -0
@@ -0,0 +1,102 @@
1
+ import { optionalArg } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import type { Rng } from "../rng/index.js";
4
+ import type { FakerSpec } from "./faker.types.js";
5
+ import { intBetween, numArg, pad, pick } from "./primitives.js";
6
+
7
+ /**
8
+ * The instant every generated date is measured from.
9
+ *
10
+ * Dates hang off a fixed anchor rather than the wall clock: `Date.now()` would
11
+ * give a different answer on every run and break the seeded replay guarantee.
12
+ */
13
+ export const ANCHOR: number = Date.UTC(2025, 0, 1);
14
+
15
+ const DAY = 86_400_000;
16
+ const YEAR = 365 * DAY;
17
+
18
+ const WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
19
+ const MONTHS = [
20
+ "January",
21
+ "February",
22
+ "March",
23
+ "April",
24
+ "May",
25
+ "June",
26
+ "July",
27
+ "August",
28
+ "September",
29
+ "October",
30
+ "November",
31
+ "December",
32
+ ];
33
+
34
+ function offsetFrom(args: { rng: Rng; min: number; max: number }): number {
35
+ return ANCHOR + intBetween({ min: args.min, max: args.max, rng: args.rng });
36
+ }
37
+
38
+ /** `YYYY-MM-DD` within five years either side of the anchor. */
39
+ function isoDate(rng: Rng): string {
40
+ const at = offsetFrom({ rng, min: -5 * YEAR, max: 5 * YEAR });
41
+ return new Date(at).toISOString().slice(0, 10);
42
+ }
43
+
44
+ function isoDateTime(rng: Rng): string {
45
+ return new Date(offsetFrom({ rng, min: -5 * YEAR, max: 5 * YEAR })).toISOString();
46
+ }
47
+
48
+ function pastDate(rng: Rng): string {
49
+ return new Date(offsetFrom({ rng, min: -5 * YEAR, max: -DAY })).toISOString();
50
+ }
51
+
52
+ function futureDate(rng: Rng): string {
53
+ return new Date(offsetFrom({ rng, min: DAY, max: 5 * YEAR })).toISOString();
54
+ }
55
+
56
+ /** `HH:MM:SS` on a 24-hour clock. */
57
+ function time(rng: Rng): string {
58
+ const part = (max: number): string => pad(intBetween({ min: 0, max, rng }), 2);
59
+ return `${part(23)}:${part(59)}:${part(59)}`;
60
+ }
61
+
62
+ /** Seconds since the epoch, the unit `exp` and `iat` claims use. */
63
+ function timestamp(rng: Rng): number {
64
+ return Math.floor(offsetFrom({ rng, min: -5 * YEAR, max: 5 * YEAR }) / 1000);
65
+ }
66
+
67
+ export const datetimeSpecs: readonly FakerSpec[] = [
68
+ { name: "date", doc: "A date as `YYYY-MM-DD`.", result: t.string, make: isoDate },
69
+ { name: "dateTime", doc: "An ISO 8601 instant.", result: t.string, make: isoDateTime },
70
+ {
71
+ name: "pastDate",
72
+ doc: "An ISO 8601 instant before the anchor.",
73
+ result: t.string,
74
+ make: pastDate,
75
+ },
76
+ {
77
+ name: "futureDate",
78
+ doc: "An ISO 8601 instant after the anchor.",
79
+ result: t.string,
80
+ make: futureDate,
81
+ },
82
+ { name: "time", doc: "A time of day as `HH:MM:SS`.", result: t.string, make: time },
83
+ { name: "timestamp", doc: "Seconds since the Unix epoch.", result: t.number, make: timestamp },
84
+ {
85
+ name: "weekday",
86
+ doc: "A day of the week.",
87
+ result: t.string,
88
+ make: (rng) => pick(WEEKDAYS, rng),
89
+ },
90
+ { name: "month", doc: "A month name.", result: t.string, make: (rng) => pick(MONTHS, rng) },
91
+ {
92
+ name: "year",
93
+ doc: "A year. `faker.year(2000, 2030)` bounds it.",
94
+ result: t.number,
95
+ args: [
96
+ optionalArg("from", t.number, "The earliest year, included."),
97
+ optionalArg("to", t.number, "The latest year, included."),
98
+ ],
99
+ make: (rng, args) =>
100
+ intBetween({ min: numArg(args, 0, 1980), max: numArg(args, 1, 2030), rng }),
101
+ },
102
+ ];
@@ -0,0 +1,24 @@
1
+ import type { ArgSpec } from "@venn-lang/sdk";
2
+ import type { TypeSpec } from "@venn-lang/types";
3
+ import type { Rng } from "../rng/index.js";
4
+
5
+ /**
6
+ * One `data.faker.*` verb: what it is called, what it means, and how to draw a
7
+ * value. A spec is plain data, so the action wiring reads the list as it stands
8
+ * and needs no edit when a category grows.
9
+ */
10
+ export interface FakerSpec {
11
+ /** The verb, without the namespace: `email`, `br.cpf`. */
12
+ name: string;
13
+ /** One line, in the user's domain. Shown on hover and in completion. */
14
+ doc: string;
15
+ /** The type it draws. A `TypeSpec`, not a name, so the checker can act on it. */
16
+ result: TypeSpec;
17
+ /**
18
+ * The positional arguments `make` reads, in order. Most verbs read none and
19
+ * leave this out. The action wiring turns it into the signature the checker sees.
20
+ */
21
+ args?: readonly ArgSpec[];
22
+ /** Draw a value. `args` carries the call's positional arguments, if any. */
23
+ make(rng: Rng, args: readonly unknown[]): unknown;
24
+ }
@@ -0,0 +1,86 @@
1
+ import { t } from "@venn-lang/types";
2
+ import type { Rng } from "../rng/index.js";
3
+ import { ibanCheck, luhnDigit } from "./check-digits.js";
4
+ import { CARD_BRANDS, CURRENCIES } from "./data/index.js";
5
+ import type { FakerSpec } from "./faker.types.js";
6
+ import { chars, digits, floatBetween, intBetween, pad, pick } from "./primitives.js";
7
+
8
+ /** A 16-digit card number that passes Luhn, grouped `#### #### #### ####`. */
9
+ function creditCard(rng: Rng): string {
10
+ const [, prefix, length] = pick(CARD_BRANDS, rng);
11
+ const base = `${prefix}${digits(length - prefix.length - 1, rng)}`;
12
+ const full = `${base}${luhnDigit(base)}`;
13
+ return (full.match(/.{1,4}/g) ?? []).join(" ");
14
+ }
15
+
16
+ function expiryDate(rng: Rng): string {
17
+ const month = intBetween({ min: 1, max: 12, rng });
18
+ return `${pad(month, 2)}/${intBetween({ min: 26, max: 34, rng })}`;
19
+ }
20
+
21
+ /** An IBAN with correct mod-97 check digits. */
22
+ function iban(rng: Rng): string {
23
+ const country = "DE";
24
+ const account = digits(18, rng);
25
+ return `${country}${ibanCheck(country, account)}${account}`;
26
+ }
27
+
28
+ function bic(rng: Rng): string {
29
+ const letters = (count: number): string =>
30
+ chars({ count, alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", rng });
31
+ return `${letters(4)}${letters(2)}${letters(2)}`;
32
+ }
33
+
34
+ export const financeSpecs: readonly FakerSpec[] = [
35
+ {
36
+ name: "creditCard",
37
+ doc: "A 16-digit card number that passes the Luhn check.",
38
+ result: t.string,
39
+ make: creditCard,
40
+ },
41
+ {
42
+ name: "cardType",
43
+ doc: "A card brand, like `Visa`.",
44
+ result: t.string,
45
+ make: (rng) => pick(CARD_BRANDS, rng)[0],
46
+ },
47
+ {
48
+ name: "cvv",
49
+ doc: "A three-digit card security code.",
50
+ result: t.string,
51
+ make: (rng) => digits(3, rng),
52
+ },
53
+ { name: "expiryDate", doc: "A card expiry as `MM/YY`.", result: t.string, make: expiryDate },
54
+ { name: "iban", doc: "An IBAN with valid check digits.", result: t.string, make: iban },
55
+ { name: "bic", doc: "A SWIFT/BIC code.", result: t.string, make: bic },
56
+ {
57
+ name: "accountNumber",
58
+ doc: "A bank account number.",
59
+ result: t.string,
60
+ make: (rng) => digits(10, rng),
61
+ },
62
+ {
63
+ name: "currencyCode",
64
+ doc: "An ISO 4217 currency code, like `BRL`.",
65
+ result: t.string,
66
+ make: (rng) => pick(CURRENCIES, rng)[0],
67
+ },
68
+ {
69
+ name: "currencySymbol",
70
+ doc: "A currency symbol, like `R$`.",
71
+ result: t.string,
72
+ make: (rng) => pick(CURRENCIES, rng)[1],
73
+ },
74
+ {
75
+ name: "currencyName",
76
+ doc: "A currency name, like `Brazilian Real`.",
77
+ result: t.string,
78
+ make: (rng) => pick(CURRENCIES, rng)[2],
79
+ },
80
+ {
81
+ name: "amount",
82
+ doc: "A monetary amount with two decimal places.",
83
+ result: t.number,
84
+ make: (rng) => floatBetween({ min: 1, max: 10000, decimals: 2, rng }),
85
+ },
86
+ ];
@@ -0,0 +1,80 @@
1
+ import { optionalArg } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import type { Rng } from "../rng/index.js";
4
+ import type { FakerSpec } from "./faker.types.js";
5
+ import { ALNUM, chars, digits, floatBetween, HEX, intBetween, numArg } from "./primitives.js";
6
+
7
+ /** An RFC 4122 v4 UUID. The version and variant bits are stamped, not drawn. */
8
+ export function uuid(rng: Rng): string {
9
+ const hex = (count: number): string => chars({ count, alphabet: HEX, rng });
10
+ const variant = "89ab".charAt(Math.floor(rng() * 4));
11
+ return `${hex(8)}-${hex(4)}-4${hex(3)}-${variant}${hex(3)}-${hex(12)}`;
12
+ }
13
+
14
+ const NANO_ALPHABET = `${ALNUM}_-`;
15
+
16
+ export const idSpecs: readonly FakerSpec[] = [
17
+ { name: "uuid", doc: "A v4 UUID.", result: t.string, make: uuid },
18
+ {
19
+ name: "nanoid",
20
+ doc: "A 21-character NanoID. `faker.nanoid(10)` sets the length.",
21
+ result: t.string,
22
+ args: [optionalArg("length", t.number, "How many characters. 21 by default.")],
23
+ make: (rng, args) => chars({ count: numArg(args, 0, 21), alphabet: NANO_ALPHABET, rng }),
24
+ },
25
+ {
26
+ name: "objectId",
27
+ doc: "A 24-character MongoDB ObjectId.",
28
+ result: t.string,
29
+ make: (rng) => chars({ count: 24, alphabet: HEX, rng }),
30
+ },
31
+ {
32
+ name: "hex",
33
+ doc: "Hex characters. `faker.hex(32)` sets how many.",
34
+ result: t.string,
35
+ args: [optionalArg("length", t.number, "How many characters.")],
36
+ make: (rng, args) => chars({ count: numArg(args, 0, 16), alphabet: HEX, rng }),
37
+ },
38
+ {
39
+ name: "token",
40
+ doc: "A 32-character opaque token.",
41
+ result: t.string,
42
+ make: (rng) => chars({ count: 32, alphabet: ALNUM, rng }),
43
+ },
44
+ {
45
+ name: "alphanumeric",
46
+ doc: "Letters and digits. `faker.alphanumeric(8)` sets how many.",
47
+ result: t.string,
48
+ args: [optionalArg("length", t.number, "How many characters.")],
49
+ make: (rng, args) => chars({ count: numArg(args, 0, 8), alphabet: ALNUM, rng }),
50
+ },
51
+ {
52
+ name: "digits",
53
+ doc: "Decimal digits. `faker.digits(6)` sets how many.",
54
+ result: t.string,
55
+ args: [optionalArg("length", t.number, "How many digits.")],
56
+ make: (rng, args) => digits(numArg(args, 0, 6), rng),
57
+ },
58
+ {
59
+ name: "int",
60
+ doc: "An integer. `faker.int(1, 10)` bounds it, inclusive.",
61
+ result: t.number,
62
+ args: [
63
+ optionalArg("min", t.number, "The lowest it may be, included."),
64
+ optionalArg("max", t.number, "The highest it may be, included."),
65
+ ],
66
+ make: (rng, args) => intBetween({ min: numArg(args, 0, 0), max: numArg(args, 1, 100), rng }),
67
+ },
68
+ {
69
+ name: "float",
70
+ doc: "A number with two decimals. `faker.float(0, 1)` bounds it.",
71
+ result: t.number,
72
+ args: [
73
+ optionalArg("min", t.number, "The lowest it may be."),
74
+ optionalArg("max", t.number, "The highest it may be."),
75
+ ],
76
+ make: (rng, args) =>
77
+ floatBetween({ min: numArg(args, 0, 0), max: numArg(args, 1, 1), decimals: 2, rng }),
78
+ },
79
+ { name: "boolean", doc: "`true` or `false`.", result: t.bool, make: (rng) => rng() < 0.5 },
80
+ ];
@@ -0,0 +1,8 @@
1
+ export { allFakerSpecs } from "./all-specs.js";
2
+ export { cnpjDigits, cpfDigits } from "./brazil-documents.js";
3
+ export { eanDigit, ibanCheck, luhnDigit } from "./check-digits.js";
4
+ export { ANCHOR } from "./datetime.js";
5
+ export type { FakerSpec } from "./faker.types.js";
6
+ export { uuid } from "./ids.js";
7
+ export { email } from "./internet.js";
8
+ export { firstName, fullName, lastName } from "./person.js";
@@ -0,0 +1,100 @@
1
+ import { t } from "@venn-lang/types";
2
+ import type { Rng } from "../rng/index.js";
3
+ import {
4
+ ADJECTIVES,
5
+ HTTP_METHODS,
6
+ HTTP_STATUSES,
7
+ MIME_TYPES,
8
+ NOUNS,
9
+ PROTOCOLS,
10
+ SAFE_DOMAINS,
11
+ TLDS,
12
+ USER_AGENTS,
13
+ } from "./data/index.js";
14
+ import type { FakerSpec } from "./faker.types.js";
15
+ import { firstName, lastName } from "./person.js";
16
+ import { ALNUM, chars, digits, HEX, intBetween, pick, slug } from "./primitives.js";
17
+
18
+ /** An email on a reserved domain, e.g. `"grace.hopper@example.test"`. It reaches nobody. */
19
+ export function email(rng: Rng): string {
20
+ const local = `${slug(firstName(rng))}.${slug(lastName(rng))}`;
21
+ return `${local}@${pick(SAFE_DOMAINS, rng)}`;
22
+ }
23
+
24
+ function username(rng: Rng): string {
25
+ return `${slug(firstName(rng))}${intBetween({ min: 1, max: 99, rng })}`;
26
+ }
27
+
28
+ function password(rng: Rng): string {
29
+ return chars({ count: 14, alphabet: `${ALNUM}!@#$%&*?`, rng });
30
+ }
31
+
32
+ function domain(rng: Rng): string {
33
+ return `${pick(ADJECTIVES, rng)}${pick(NOUNS, rng)}.${pick(TLDS, rng)}`;
34
+ }
35
+
36
+ function url(rng: Rng): string {
37
+ return `${pick(PROTOCOLS, rng)}://${domain(rng)}/${pick(NOUNS, rng)}`;
38
+ }
39
+
40
+ function ipv4(rng: Rng): string {
41
+ const octet = (): number => intBetween({ min: 1, max: 254, rng });
42
+ return `${octet()}.${octet()}.${octet()}.${octet()}`;
43
+ }
44
+
45
+ function ipv6(rng: Rng): string {
46
+ const group = (): string => chars({ count: 4, alphabet: HEX, rng });
47
+ return Array.from({ length: 8 }, group).join(":");
48
+ }
49
+
50
+ function mac(rng: Rng): string {
51
+ const pair = (): string => chars({ count: 2, alphabet: HEX, rng }).toUpperCase();
52
+ return Array.from({ length: 6 }, pair).join(":");
53
+ }
54
+
55
+ export const internetSpecs: readonly FakerSpec[] = [
56
+ { name: "email", doc: "An email address on a reserved domain.", result: t.string, make: email },
57
+ { name: "username", doc: "A login name.", result: t.string, make: username },
58
+ { name: "password", doc: "A 14-character password.", result: t.string, make: password },
59
+ { name: "domain", doc: "A domain name.", result: t.string, make: domain },
60
+ { name: "url", doc: "An absolute URL.", result: t.string, make: url },
61
+ { name: "ipv4", doc: "An IPv4 address.", result: t.string, make: ipv4 },
62
+ { name: "ipv6", doc: "An IPv6 address.", result: t.string, make: ipv6 },
63
+ { name: "mac", doc: "A MAC address.", result: t.string, make: mac },
64
+ {
65
+ name: "port",
66
+ doc: "An unprivileged TCP port.",
67
+ result: t.number,
68
+ make: (rng) => intBetween({ min: 1024, max: 65535, rng }),
69
+ },
70
+ {
71
+ name: "userAgent",
72
+ doc: "A browser User-Agent string.",
73
+ result: t.string,
74
+ make: (rng) => pick(USER_AGENTS, rng),
75
+ },
76
+ {
77
+ name: "slug",
78
+ doc: "A URL slug, like `bright-harbour-417`.",
79
+ result: t.string,
80
+ make: (rng) => `${pick(ADJECTIVES, rng)}-${pick(NOUNS, rng)}-${digits(3, rng)}`,
81
+ },
82
+ {
83
+ name: "httpMethod",
84
+ doc: "An HTTP verb.",
85
+ result: t.string,
86
+ make: (rng) => pick(HTTP_METHODS, rng),
87
+ },
88
+ {
89
+ name: "httpStatus",
90
+ doc: "An HTTP status code that servers really return.",
91
+ result: t.number,
92
+ make: (rng) => pick(HTTP_STATUSES, rng),
93
+ },
94
+ {
95
+ name: "mimeType",
96
+ doc: "A MIME type.",
97
+ result: t.string,
98
+ make: (rng) => pick(MIME_TYPES, rng),
99
+ },
100
+ ];
@@ -0,0 +1,88 @@
1
+ import { t } from "@venn-lang/types";
2
+ import type { Rng } from "../rng/index.js";
3
+ import {
4
+ FIRST_NAMES,
5
+ GENDERS,
6
+ JOB_TITLES,
7
+ LAST_NAMES,
8
+ NAME_PREFIXES,
9
+ NAME_SUFFIXES,
10
+ } from "./data/index.js";
11
+ import type { FakerSpec } from "./faker.types.js";
12
+ import { digits, intBetween, pad, pick } from "./primitives.js";
13
+
14
+ /** A given name, e.g. `"Grace"`. */
15
+ export function firstName(rng: Rng): string {
16
+ return pick(FIRST_NAMES, rng);
17
+ }
18
+
19
+ /** A family name, e.g. `"Hopper"`. */
20
+ export function lastName(rng: Rng): string {
21
+ return pick(LAST_NAMES, rng);
22
+ }
23
+
24
+ /** A full name, e.g. `"Grace Hopper"`. */
25
+ export function fullName(rng: Rng): string {
26
+ return `${firstName(rng)} ${lastName(rng)}`;
27
+ }
28
+
29
+ function birthDate(rng: Rng): string {
30
+ const year = intBetween({ min: 1950, max: 2006, rng });
31
+ const month = intBetween({ min: 1, max: 12, rng });
32
+ const day = intBetween({ min: 1, max: 28, rng });
33
+ return `${year}-${pad(month, 2)}-${pad(day, 2)}`;
34
+ }
35
+
36
+ /** `+1 555 01xx`, the range reserved for fiction, so it can never reach anyone. */
37
+ function phone(rng: Rng): string {
38
+ return `+1 555 01${digits(2, rng)}`;
39
+ }
40
+
41
+ export const personSpecs: readonly FakerSpec[] = [
42
+ { name: "firstName", doc: "A given name.", result: t.string, make: firstName },
43
+ { name: "lastName", doc: "A family name.", result: t.string, make: lastName },
44
+ { name: "name", doc: "A full name.", result: t.string, make: fullName },
45
+ { name: "fullName", doc: "A full name.", result: t.string, make: fullName },
46
+ {
47
+ name: "prefix",
48
+ doc: "An honorific that precedes a name, like `Dra.`.",
49
+ result: t.string,
50
+ make: (rng) => pick(NAME_PREFIXES, rng),
51
+ },
52
+ {
53
+ name: "suffix",
54
+ doc: "A generational suffix that follows a name, like `Neto`.",
55
+ result: t.string,
56
+ make: (rng) => pick(NAME_SUFFIXES, rng),
57
+ },
58
+ {
59
+ name: "gender",
60
+ doc: "A gender value shaped the way most forms model them.",
61
+ result: t.string,
62
+ make: (rng) => pick(GENDERS, rng),
63
+ },
64
+ {
65
+ name: "jobTitle",
66
+ doc: "A job title, like `Site Reliability Engineer`.",
67
+ result: t.string,
68
+ make: (rng) => pick(JOB_TITLES, rng),
69
+ },
70
+ {
71
+ name: "age",
72
+ doc: "An adult age, between 18 and 80.",
73
+ result: t.number,
74
+ make: (rng) => intBetween({ min: 18, max: 80, rng }),
75
+ },
76
+ {
77
+ name: "birthDate",
78
+ doc: "A birth date as `YYYY-MM-DD`.",
79
+ result: t.string,
80
+ make: birthDate,
81
+ },
82
+ {
83
+ name: "phone",
84
+ doc: "An international phone number in the range reserved for fiction.",
85
+ result: t.string,
86
+ make: phone,
87
+ },
88
+ ];
@@ -0,0 +1,77 @@
1
+ import type { Rng } from "../rng/index.js";
2
+
3
+ const COMBINING = /[\u0300-\u036f]/g;
4
+
5
+ export const DIGITS = "0123456789";
6
+ export const HEX = "0123456789abcdef";
7
+ export const LOWER = "abcdefghijklmnopqrstuvwxyz";
8
+ export const ALNUM: string = `${LOWER}${LOWER.toUpperCase()}${DIGITS}`;
9
+
10
+ /** Pick one item from a list. Drawing from an empty list is a programming error. */
11
+ export function pick<T>(items: readonly T[], rng: Rng): T {
12
+ const chosen = items[Math.floor(rng() * items.length)];
13
+ if (chosen === undefined) throw new Error("faker: cannot pick from an empty list");
14
+ return chosen;
15
+ }
16
+
17
+ /** An integer in the inclusive range [min, max]. */
18
+ export function intBetween(args: { min: number; max: number; rng: Rng }): number {
19
+ return args.min + Math.floor(args.rng() * (args.max - args.min + 1));
20
+ }
21
+
22
+ /** A number in [min, max), rounded to `decimals` places. */
23
+ export function floatBetween(args: {
24
+ min: number;
25
+ max: number;
26
+ decimals: number;
27
+ rng: Rng;
28
+ }): number {
29
+ const raw = args.min + args.rng() * (args.max - args.min);
30
+ return Number(raw.toFixed(args.decimals));
31
+ }
32
+
33
+ /** `count` characters drawn from `alphabet`. */
34
+ export function chars(args: { count: number; alphabet: string; rng: Rng }): string {
35
+ let out = "";
36
+ for (let index = 0; index < args.count; index += 1) {
37
+ out += args.alphabet.charAt(Math.floor(args.rng() * args.alphabet.length));
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /** `count` decimal digits, e.g. `"40721"`. */
43
+ export function digits(count: number, rng: Rng): string {
44
+ return chars({ count, alphabet: DIGITS, rng });
45
+ }
46
+
47
+ /** Upper-case the first character, leaving the rest alone. */
48
+ export function capitalize(word: string): string {
49
+ return word.charAt(0).toUpperCase() + word.slice(1);
50
+ }
51
+
52
+ /** Zero-pad a number to `width` digits. */
53
+ export function pad(value: number, width: number): string {
54
+ return String(value).padStart(width, "0");
55
+ }
56
+
57
+ /** Strip diacritics so a name can become an email local part or a URL slug. */
58
+ export function ascii(text: string): string {
59
+ return text.normalize("NFD").replace(COMBINING, "");
60
+ }
61
+
62
+ /** ASCII-safe lowercase slug: `"João Gonçalves"` → `"joao-goncalves"`. */
63
+ export function slug(text: string): string {
64
+ const plain = ascii(text).toLowerCase();
65
+ return plain.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
66
+ }
67
+
68
+ /** Read a positional argument as a number, falling back when it is absent. */
69
+ export function numArg(args: readonly unknown[], index: number, fallback: number): number {
70
+ const raw = Number(args[index]);
71
+ return args[index] === undefined || Number.isNaN(raw) ? fallback : raw;
72
+ }
73
+
74
+ /** Build `count` values, each drawn independently. */
75
+ export function times<T>(count: number, make: () => T): T[] {
76
+ return Array.from({ length: count }, make);
77
+ }
@@ -0,0 +1,56 @@
1
+ import { optionalArg } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import type { Rng } from "../rng/index.js";
4
+ import { ADJECTIVES, LOREM, NOUNS } from "./data/index.js";
5
+ import type { FakerSpec } from "./faker.types.js";
6
+ import { capitalize, intBetween, numArg, pick, times } from "./primitives.js";
7
+
8
+ function words(rng: Rng, count: number): string {
9
+ return times(count, () => pick(LOREM, rng)).join(" ");
10
+ }
11
+
12
+ /** A capitalised sentence of 6 to 14 words, ending in a full stop. */
13
+ function sentence(rng: Rng): string {
14
+ return `${capitalize(words(rng, intBetween({ min: 6, max: 14, rng })))}.`;
15
+ }
16
+
17
+ function sentences(rng: Rng, count: number): string {
18
+ return times(count, () => sentence(rng)).join(" ");
19
+ }
20
+
21
+ function paragraph(rng: Rng): string {
22
+ return sentences(rng, intBetween({ min: 3, max: 6, rng }));
23
+ }
24
+
25
+ /** A headline in title case, e.g. `"Bright Harbour"`. */
26
+ function title(rng: Rng): string {
27
+ return `${capitalize(pick(ADJECTIVES, rng))} ${capitalize(pick(NOUNS, rng))}`;
28
+ }
29
+
30
+ export const textSpecs: readonly FakerSpec[] = [
31
+ { name: "word", doc: "A single word.", result: t.string, make: (rng) => pick(LOREM, rng) },
32
+ {
33
+ name: "words",
34
+ doc: "Several words. `faker.words(5)` sets how many.",
35
+ result: t.string,
36
+ args: [optionalArg("count", t.number, "How many words.")],
37
+ make: (rng, args) => words(rng, numArg(args, 0, 3)),
38
+ },
39
+ { name: "sentence", doc: "One sentence.", result: t.string, make: sentence },
40
+ {
41
+ name: "sentences",
42
+ doc: "Several sentences. `faker.sentences(3)` sets how many.",
43
+ result: t.string,
44
+ args: [optionalArg("count", t.number, "How many sentences.")],
45
+ make: (rng, args) => sentences(rng, numArg(args, 0, 3)),
46
+ },
47
+ { name: "paragraph", doc: "One paragraph.", result: t.string, make: paragraph },
48
+ {
49
+ name: "paragraphs",
50
+ doc: "Several paragraphs, separated by blank lines. `faker.paragraphs(4)` sets how many.",
51
+ result: t.string,
52
+ args: [optionalArg("count", t.number, "How many paragraphs.")],
53
+ make: (rng, args) => times(numArg(args, 0, 2), () => paragraph(rng)).join("\n\n"),
54
+ },
55
+ { name: "title", doc: "A short headline in title case.", result: t.string, make: title },
56
+ ];
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ // @venn-lang/data: pure, deterministic test-data generators (faker, oneOf, range,
2
+ // shuffle, csv, json). A module-level mulberry32 PRNG (seed 1) keeps output reproducible.
3
+
4
+ export { dataActions } from "./actions/index.js";
5
+ export * from "./csv/index.js";
6
+ export * from "./faker/index.js";
7
+ export { dataPlugin, dataPlugin as default } from "./plugin.js";
8
+ export * from "./rng/index.js";
package/src/plugin.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
2
+ import { dataActions } from "./actions/index.js";
3
+ import { dataTypeDefs } from "./types.js";
4
+
5
+ /**
6
+ * The `data` plugin: pure, deterministic test-data generators.
7
+ *
8
+ * Every verb draws from a seeded module-level PRNG, so the same script replays the
9
+ * same values. Nothing here does I/O, so there is no capability and no port to bind.
10
+ */
11
+ export const dataPlugin: PluginDefinition = definePlugin({
12
+ name: "venn/data",
13
+ version: "0.0.0",
14
+ namespace: "data",
15
+ actions: dataActions,
16
+ typeDefs: dataTypeDefs,
17
+ });
@@ -0,0 +1,4 @@
1
+ export { mulberry32 } from "./mulberry32.js";
2
+ export type { Rng } from "./rng.types.js";
3
+ export { resetRng, rng } from "./shared-rng.js";
4
+ export { shuffleWith } from "./shuffle.js";
@@ -0,0 +1,18 @@
1
+ import type { Rng } from "./rng.types.js";
2
+
3
+ /**
4
+ * Mulberry32: a tiny, fast, fully deterministic PRNG.
5
+ *
6
+ * @param seed Any number. It is coerced to a uint32, so the same seed always
7
+ * yields the same stream.
8
+ * @returns A generator producing the next float in [0, 1) on each call.
9
+ */
10
+ export function mulberry32(seed: number): Rng {
11
+ let state = seed >>> 0;
12
+ return () => {
13
+ state = (state + 0x6d2b79f5) | 0;
14
+ let t = Math.imul(state ^ (state >>> 15), 1 | state);
15
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
16
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17
+ };
18
+ }
@@ -0,0 +1,2 @@
1
+ /** A deterministic pseudo-random generator: each call yields the next float in [0, 1). */
2
+ export type Rng = () => number;