Skip to content
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
# @jsonjoy.com/json-random
# `json-random`

The `json-random` library lets you generate random JSON values.

- `randomString` - generates a random string following a string token template.
- `TemplateJson` - generates random JSON following a template.
- `RandomJson` - generates random JSON by generating new nodes and inserting them into random positions in the JSON.
- `int` - generates a random integer.
- `deterministic(seed, () => {})` - fixates `Math.random()` such that code in callback generates a deterministic value.


## Use Cases
Expand All @@ -25,12 +33,18 @@ const optimizedFunction = codegen.compile();
```


# json-random
## Reference

The `json-random` library lets you generate random JSON values.
### `randomString`

TODO: ...


### `TemplateJson`

TODO: ...

## Usage
### `RandomJson`

Generate a random JSON object.

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
"peerDependencies": {
"tslib": "2"
},
"dependencies": {},
"dependencies": {
"@jsonjoy.com/buffers": "^1.0.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/benchmark": "^2.1.2",
Expand Down
4 changes: 2 additions & 2 deletions src/RandomJson.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {randomString, Token} from './string';
import {randomString, type Token} from './string';

type JsonValue = unknown;

Expand Down Expand Up @@ -170,7 +170,7 @@ export class RandomJson {
: Math.random() < 0.2
? Math.round(0xffff * (2 * Math.random() - 1))
: Math.round(Number.MAX_SAFE_INTEGER * (2 * Math.random() - 1));
if (num === -0) return 0;
if (num === 0) return 0;
return num;
}

Expand Down
56 changes: 56 additions & 0 deletions src/__demos__/map-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Run with:
*
* npx ts-node src/__demos__/map-demo.ts
*/

import {TemplateJson} from '../structured/TemplateJson';

console.log('=== Map Template Demo ===\n');

// Basic map usage
console.log('1. Basic map with shorthand:');
const basicMap = TemplateJson.gen('map');
console.log(JSON.stringify(basicMap, null, 2));

// Map with custom key tokens and values
console.log('\n2. Map with custom user IDs and profile data:');
const userMap = TemplateJson.gen([
'map',
['list', 'user_', ['pick', '001', '002', '003', '004', '005']],
[
'obj',
[
['name', ['str', ['list', ['pick', 'John', 'Jane', 'Bob', 'Alice'], ' ', ['pick', 'Doe', 'Smith', 'Johnson']]]],
['age', ['int', 18, 65]],
['active', 'bool'],
],
],
2,
4,
]);
console.log(JSON.stringify(userMap, null, 2));

// Map with complex nested structures
console.log('\n3. Map with API endpoints and their configurations:');
const apiMap = TemplateJson.gen([
'map',
['list', 'api/', ['pick', 'users', 'posts', 'comments', 'auth']],
[
'obj',
[
['method', ['str', ['pick', 'GET', 'POST', 'PUT', 'DELETE']]],
['timeout', ['int', 1000, 5000]],
['retries', ['int', 0, 3]],
['auth_required', 'bool'],
],
],
3,
3,
]);
console.log(JSON.stringify(apiMap, null, 2));

// Map with guaranteed size
console.log('\n4. Map with exactly 2 entries:');
const fixedMap = TemplateJson.gen(['map', ['pick', 'key1', 'key2', 'key3'], ['or', 'str', 'int', 'bool'], 2, 2]);
console.log(JSON.stringify(fixedMap, null, 2));
8 changes: 1 addition & 7 deletions src/__tests__/RandomJson.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,7 @@ test('random strings can be converted to UTF-8', () => {
test('can specify string generation schema', () => {
const str = RandomJson.generate({
rootNode: 'string',
strings: [
'list',
[
['repeat', 2, 2, 'xx'],
['pick', ['y']],
],
],
strings: ['list', ['repeat', 2, 2, 'xx'], ['pick', 'y']],
});
expect(str).toBe('xxxxy');
});
6 changes: 6 additions & 0 deletions src/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const resetMathRandom = (seed = 123456789) => {
Math.random = () => {
seed = (seed * 48271) % 2147483647;
return (seed - 1) / 2147483646;
};
};
35 changes: 22 additions & 13 deletions src/__tests__/string.spec.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,50 @@
import {randomString, Token} from '../string';
import {randomString, type Token} from '../string';
import {deterministic} from '../util';

// Tests for randomString
describe('randomString', () => {
it('should pick a random string from the array', () => {
const token: Token = ['pick', ['apple', 'banana', 'cherry']];
const token: Token = ['pick', 'apple', 'banana', 'cherry'];
const result = randomString(token);
expect(['apple', 'banana', 'cherry']).toContain(result);
});

it('should repeat a pattern a random number of times', () => {
const token: Token = ['repeat', 2, 5, ['pick', ['x', 'y', 'z', ' ']]];
const token: Token = ['repeat', 2, 5, ['pick', 'x', 'y', 'z', ' ']];
const result = randomString(token);
expect(result.length).toBeGreaterThanOrEqual(2);
expect(result.length).toBeLessThanOrEqual(5);
});

it('should pick a random character from the Unicode range', () => {
const token: Token = ['range', 65, 90]; // A-Z
const token: Token = ['char', 65, 90]; // A-Z
const result = randomString(token);
expect(result).toMatch(/^[A-Z]$/);
});

// test tlist token
it('should pick a random character from the Unicode range three times', () => {
const token: Token = ['char', 65, 90, 3]; // A-Z
const result = randomString(token);
expect(result).toMatch(/^[A-Z]{3}$/);
});

it('executes a list of tokens', () => {
const token: Token = [
'list',
[
['pick', ['monkey', 'dog', 'cat']],
['pick', [' ']],
['pick', ['ate', 'threw', 'picked']],
['pick', [' ']],
['pick', ['apple', 'banana', 'cherry']],
],
['pick', 'monkey', 'dog', 'cat'],
['pick', ' '],
['pick', 'ate', 'threw', 'picked'],
['pick', ' '],
['pick', 'apple', 'banana', 'cherry'],
];
const result = randomString(token);
expect(/monkey|dog|cat/.test(result)).toBe(true);
expect(/ate|threw|picked/.test(result)).toBe(true);
expect(/apple|banana|cherry/.test(result)).toBe(true);
});

it('can nest picks', () => {
const token: Token = ['pick', ['pick', 'monkey', 'dog', 'cat'], ['pick', 'banana', 'apple']];
const str = deterministic(123, () => randomString(token));
expect(str).toBe('dog');
});
});
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export {deterministic, rnd} from './util';
export * from './RandomJson';
export * from './number';
export * from './string';
export * from './structured';
5 changes: 5 additions & 0 deletions src/number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const int = (min: number, max: number): number => {
let int = Math.round(Math.random() * (max - min) + min);
int = Math.max(min, Math.min(max, int));
return int;
};
41 changes: 22 additions & 19 deletions src/string.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
/**
* Tokens used to specify random string generation options
*/
export type Token = TokenLiteral | TokenPick | TokenRepeat | TokenRange | TokenList;
export type Token = TokenLiteral | TokenPick | TokenRepeat | TokenChar | TokenList;

/**
* A string literal to use as-is.
*/
export type TokenLiteral = string;

/**
* Picks a random string from the provided array of strings.
* The `from` array can contain any number of strings.
* Picks a random token from the provided tokens.
*/
export type TokenPick = [type: 'pick', from: string[]];
export type TokenPick = [type: 'pick', ...from: Token[]];

/**
* Repeats `pattern` a random number of times between `min` and `max`.
Expand All @@ -21,13 +20,14 @@ export type TokenRepeat = [type: 'repeat', min: number, max: number, pattern: To

/**
* Specifies a Unicode code point range from which to pick a random character.
* The `count` parameter specifies how many characters to pick, defaults to 1.
*/
export type TokenRange = [type: 'range', min: number, max: number];
export type TokenChar = [type: 'char', min: number, max: number, count?: number];

/**
* Executes a list of `what` tokens in sequence.
* Executes a list of `every` tokens in sequence.
*/
export type TokenList = [type: 'list', what: Token[]];
export type TokenList = [type: 'list', ...every: Token[]];

/**
* Generates a random string based on the provided token.
Expand All @@ -39,26 +39,29 @@ export function randomString(token: Token): string {
const rnd = Math.random();
switch (token[0]) {
case 'pick': {
const set = token[1];
return set[Math.floor(rnd * set.length)];
const [, ...from] = token;
return randomString(from[Math.floor(rnd * from.length)]);
}
case 'repeat': {
const min = token[1];
const max = token[2];
const pattern = token[3];
const [, min, max, pattern] = token;
const count = Math.floor(rnd * (max - min + 1)) + min;
let str = '';
for (let i = 0; i < count; i++) str += randomString(pattern);
return str;
}
case 'range': {
const min = token[1];
const max = token[2];
const codePoint = Math.floor(rnd * (max - min + 1)) + min;
return String.fromCodePoint(codePoint);
case 'char': {
const [, min, max, count = 1] = token;
let str = '';
for (let i = 0; i < count; i++) {
const codePoint = Math.floor(rnd * (max - min + 1)) + min;
str += String.fromCodePoint(codePoint);
}
return str;
}
case 'list': {
const [, ...every] = token;
return every.map(randomString).join('');
}
case 'list':
return token[1].map(randomString).join('');
default:
throw new Error('Invalid token type');
}
Expand Down
Loading