Skip to content

Commit ca5cec6

Browse files
authored
Merge pull request #4 from TouK/dev
release
2 parents e9e9385 + 8d35085 commit ca5cec6

6 files changed

Lines changed: 145 additions & 137 deletions

File tree

cli/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## [1.0.2-beta.1](https://github.com/TouK/nu-cloud-lab/compare/v1.0.1...v1.0.2-beta.1) (2026-02-18)
2+
3+
### Bug Fixes
4+
5+
* fixed faker templates, added short prefix `::` ([11766ff](https://github.com/TouK/nu-cloud-lab/commit/11766ff2dba478e45bea8e2b670a4250538b0b14))
6+
17
## [1.0.1](https://github.com/TouK/nu-cloud-lab/compare/v1.0.0...v1.0.1) (2026-02-18)
28

39
### Bug Fixes

cli/README.md

Lines changed: 72 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,9 @@ Templates define the structure of generated messages using faker.js for realisti
277277
The CLI includes a default template with faker.js support:
278278

279279
```yaml
280-
name: "faker:person.fullName"
281-
email: "faker:internet.email"
282-
age: "faker:number.int(18,65)"
280+
name: "::person.fullName"
281+
email: "::internet.email"
282+
age: "::number.int({min:18, max:65})"
283283
timestamp: "current_timestamp"
284284
```
285285

@@ -289,11 +289,12 @@ Create your own template file:
289289

290290
```yaml
291291
# my-template.yaml
292-
userId: "faker:string.uuid"
293-
username: "faker:internet.userName"
294-
company: "faker:company.name"
295-
location: "faker:location.city"
296-
price: "faker:commerce.price(10,1000)"
292+
userId: "::string.uuid"
293+
username: "::internet.userName"
294+
company: "::company.name"
295+
location: "::location.city"
296+
price: "::commerce.price({min:10, max:1000})"
297+
status: '::helpers.arrayElement(["active","pending","suspended"])'
297298
```
298299

299300
**Use it:**
@@ -310,63 +311,81 @@ nu-cli send --template ./my-template.yaml
310311

311312
### Faker.js Syntax
312313

313-
Supports full [faker.js API](https://fakerjs.dev/api/):
314+
Full [faker.js API](https://fakerjs.dev/api/) support using `::` prefix. Parameters are parsed as JSON and spread as `...args` to faker functions.
314315

315-
- `faker:person.fullName` → Random full name
316-
- `faker:internet.email` → Random email
317-
- `faker:number.int(18,65)` → Random integer between 18 and 65
318-
- `faker:company.name` → Random company name
319-
- `faker:location.streetAddress` → Random street address
320-
- `faker:string.uuid` → Random UUID
321-
- `faker:commerce.price(10,1000)` → Random price between 10 and 1000
322-
323-
### Legacy Syntax (still supported)
316+
**Simple calls (no parameters):**
317+
```yaml
318+
name: "::person.fullName"
319+
email: "::internet.email"
320+
company: "::company.name"
321+
address: "::location.streetAddress"
322+
uuid: "::string.uuid"
323+
```
324324

325-
- `random_name` → Random name from built-in list
326-
- `random_int(1,100)` → Random integer
327-
- `current_timestamp` → ISO 8601 timestamp
325+
**With parameters (use JSON syntax, wrap in single quotes in YAML):**
326+
```yaml
327+
# Object parameters
328+
age: "::number.int({min:18, max:65})"
329+
price: "::commerce.price({min:10, max:1000})"
328330
329-
## Message Templates
331+
# Single argument (array)
332+
event: '::helpers.arrayElement(["login","logout","purchase"])'
330333
331-
Messages are generated from templates defined in `src/lib/template/generator.ts`. To customize message structure, edit the `MESSAGE_TEMPLATE` constant:
334+
# Multiple arguments (array, options object)
335+
tags: '::helpers.arrayElements(["tech","sports","music"], {min:2, max:4})'
332336
333-
```typescript
334-
export const MESSAGE_TEMPLATE: TemplateObject = {
335-
"name": "random_name",
336-
};
337+
# Multiple arguments (coordinates, distance, isMetric)
338+
location: '::location.nearbyGPSCoordinate([52.52, 13.40], 10, true)'
337339
```
338340

339-
### Example: Complex template
340-
341-
```typescript
342-
export const MESSAGE_TEMPLATE = {
343-
user: {
344-
name: "random_name",
345-
city: "random_city"
346-
},
347-
order: {
348-
product: "random_product",
349-
quantity: "random_int(1,5)",
350-
status: "random_status",
351-
timestamp: "current_timestamp"
352-
}
353-
};
354-
```
341+
**How it works:**
342+
- `::category.method` maps to `faker.category.method()`
343+
- Parameters are parsed as JSON: `({min:18,max:65})` → `fn({min:18, max:65})`
344+
- Supports any faker.js function signature exactly as documented
355345

356-
### Available placeholders
346+
**Special values:**
347+
- `current_timestamp` → ISO 8601 timestamp
357348

358-
- `random_name` - Random person name from predefined list
359-
- `random_city` - Random city name
360-
- `random_product` - Random product name
361-
- `random_status` - Random status (pending, completed, failed, in_progress)
362-
- `random_int(min,max)` - Random integer in range (e.g., `random_int(1,10)`)
363-
- `current_timestamp` - ISO 8601 timestamp (e.g., `2024-02-16T10:30:00.000Z`)
349+
### Advanced Examples
364350

365-
After modifying the template, rebuild the CLI:
366-
```bash
367-
npm run build
351+
**Complete event template:**
352+
```yaml
353+
# events.yaml
354+
userId: "::string.uuid"
355+
eventType: '::helpers.arrayElement(["login","logout","purchase","click"])'
356+
timestamp: "current_timestamp"
357+
user:
358+
name: "::person.fullName"
359+
email: "::internet.email"
360+
age: "::number.int({min:18, max:65})"
361+
metadata:
362+
ip: "::internet.ip"
363+
device: '::helpers.arrayElement(["mobile","desktop","tablet"])'
364+
browser: '::helpers.arrayElement(["Chrome","Firefox","Safari","Edge"])'
365+
location: "::location.city"
366+
coordinates: '::location.nearbyGPSCoordinate([52.52, 13.40], 10, true)'
367+
```
368+
369+
**E-commerce order:**
370+
```yaml
371+
orderId: "::string.uuid"
372+
products: '::helpers.arrayElements(["laptop","phone","tablet","watch","headphones"], {min:1, max:3})'
373+
totalPrice: "::commerce.price({min:50, max:5000})"
374+
status: '::helpers.arrayElement(["pending","processing","shipped","delivered"])'
375+
customer:
376+
id: "::string.uuid"
377+
name: "::person.fullName"
378+
email: "::internet.email"
379+
shippingAddress:
380+
street: "::location.streetAddress"
381+
city: "::location.city"
382+
country: "::location.country"
383+
zipCode: "::location.zipCode"
384+
createdAt: "current_timestamp"
368385
```
369386

387+
388+
370389
## Examples
371390

372391
### Continuous production with custom delay

cli/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nussknacker/cli",
3-
"version": "1.0.1",
3+
"version": "1.0.2-beta.1",
44
"description": "CLI tool for Nussknacker Cloud - produce and consume messages",
55
"type": "module",
66
"bin": {

cli/src/lib/template/random.ts

Lines changed: 45 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,45 @@
11
import { faker } from '@faker-js/faker';
22
import { logger } from '../../utils/logger.js';
33

4-
export const SAMPLE_DATA = {
5-
names: ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Hannah"],
6-
cities: ["New York", "London", "Tokyo", "Paris", "Berlin", "Sydney"],
7-
products: ["Laptop", "Phone", "Tablet", "Watch", "Headphones"],
8-
statuses: ["pending", "completed", "failed", "in_progress"]
9-
} as const;
10-
114
export function getRandomValue(fieldType: string): any {
12-
// Faker.js syntax support
13-
if (fieldType.startsWith('faker:')) {
5+
// Faker.js syntax: faker:category.method or ::category.method
6+
if (fieldType.startsWith('faker:') || fieldType.startsWith('::')) {
147
return parseFakerExpression(fieldType);
158
}
169

17-
// Legacy syntax (backward compatibility)
18-
if (fieldType === "random_name") {
19-
return SAMPLE_DATA.names[Math.floor(Math.random() * SAMPLE_DATA.names.length)];
20-
} else if (fieldType === "random_city") {
21-
return SAMPLE_DATA.cities[Math.floor(Math.random() * SAMPLE_DATA.cities.length)];
22-
} else if (fieldType === "random_product") {
23-
return SAMPLE_DATA.products[Math.floor(Math.random() * SAMPLE_DATA.products.length)];
24-
} else if (fieldType === "random_status") {
25-
return SAMPLE_DATA.statuses[Math.floor(Math.random() * SAMPLE_DATA.statuses.length)];
26-
} else if (fieldType === "current_timestamp") {
10+
// Special: current_timestamp
11+
if (fieldType === "current_timestamp") {
2712
return new Date().toISOString();
28-
} else if (fieldType.startsWith("random_int(")) {
29-
const [min, max] = fieldType.slice(11, -1).split(',').map(Number);
30-
return Math.floor(Math.random() * (max - min + 1)) + min;
3113
}
14+
15+
// No transformation - return as-is
3216
return fieldType;
3317
}
3418

3519
/**
3620
* Parse and execute faker.js expressions
37-
*
21+
*
3822
* Examples:
23+
* "::person.fullName" -> faker.person.fullName()
24+
* "::number.int({min:18,max:65})" -> faker.number.int({min:18, max:65})
25+
* "::helpers.arrayElement([1,2,3])" -> faker.helpers.arrayElement([1,2,3])
26+
* '::helpers.arrayElements([1,2,3],{min:2,max:4})' -> faker.helpers.arrayElements([1,2,3], {min:2, max:4})
27+
*
28+
* Legacy "faker:" prefix still supported:
3929
* "faker:person.fullName" -> faker.person.fullName()
40-
* "faker:number.int(18,65)" -> faker.number.int({min: 18, max: 65})
41-
* "faker:internet.email" -> faker.internet.email()
4230
*/
4331
function parseFakerExpression(expr: string): any {
44-
// Pattern: faker:category.method or faker:category.method(params)
45-
const match = expr.match(/^faker:([a-zA-Z.]+)(?:\(([^)]*)\))?$/);
46-
32+
// Pattern: ::category.method or faker:category.method (both supported)
33+
const match = expr.match(/^(?:faker:|::)([a-zA-Z.]+)(?:\(([^)]*)\))?$/);
34+
4735
if (!match) {
4836
logger.warn(`Invalid faker syntax: ${expr}`);
4937
return expr;
5038
}
51-
39+
5240
const [, path, params] = match;
5341
const parts = path.split('.');
54-
42+
5543
// Navigate to faker method: faker.person.fullName
5644
let fn: any = faker;
5745
for (const part of parts) {
@@ -61,51 +49,45 @@ function parseFakerExpression(expr: string): any {
6149
return expr;
6250
}
6351
}
64-
52+
6553
if (typeof fn !== 'function') {
6654
logger.warn(`Faker path is not a function: ${path}`);
6755
return expr;
6856
}
69-
57+
7058
// Call with params if present
71-
if (params && params.trim()) {
59+
if (params?.trim()) {
7260
const args = parseFakerParams(params, path);
73-
return fn(args);
61+
return fn(...args);
7462
}
75-
63+
7664
// Call without params
7765
return fn();
7866
}
7967

8068
/**
81-
* Parse faker parameters
82-
*
83-
* Examples:
84-
* "18,65" (for number.int) -> {min: 18, max: 65}
85-
* "5" (for string methods) -> {length: 5}
69+
* Parse faker parameters as JSON array for spreading
70+
*
71+
* Treats params as JSON array and returns array of arguments:
72+
* "[1,2,3]" -> [[1,2,3]]
73+
* "[1,2,3],{min:2,max:4}" -> [[1,2,3], {min:2, max:4}]
74+
* "{min:18,max:65}" -> [{min:18, max:65}]
75+
*
76+
* This allows spreading as ...args to faker functions.
8677
*/
87-
function parseFakerParams(params: string, fakerPath: string): any {
88-
const values = params.split(',').map(v => v.trim());
89-
90-
// Special handling for number.int/float (min,max)
91-
if (fakerPath.startsWith('number.')) {
92-
if (values.length === 2) {
93-
return {
94-
min: parseFloat(values[0]),
95-
max: parseFloat(values[1])
96-
};
97-
}
98-
if (values.length === 1) {
99-
return { max: parseFloat(values[0]) };
100-
}
101-
}
102-
103-
// Default: single numeric param as length/count
104-
if (values.length === 1 && !isNaN(parseFloat(values[0]))) {
105-
return { length: parseInt(values[0], 10) };
78+
function parseFakerParams(params: string, _fakerPath: string): any[] {
79+
// Parse as JSON array
80+
try {
81+
const wrapped = `[${params}]`;
82+
83+
// Support relaxed JSON syntax (unquoted keys)
84+
const relaxed = wrapped.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
85+
86+
const parsed = JSON.parse(relaxed);
87+
return Array.isArray(parsed) ? parsed : [parsed];
88+
} catch (e) {
89+
// JSON parse failed
90+
logger.warn(`Failed to parse faker params as JSON: ${params}`);
91+
return [];
10692
}
107-
108-
// Fallback: return as object
109-
logger.warn(`Unsure how to parse params for ${fakerPath}: ${params}`);
110-
return {};
11193
}
Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
# Default message template for Nu Cloud
2-
#
3-
# This template demonstrates faker.js syntax for generating random test data.
2+
#
3+
# Faker.js integration for generating realistic test data.
44
# Full API reference: https://fakerjs.dev/api/
55
#
66
# Syntax:
7-
# faker:category.method -> faker.person.fullName()
8-
# faker:category.method(params) -> faker.number.int(18,65)
7+
# ::category.method -> faker.person.fullName()
8+
# ::category.method({...}) -> faker.number.int({min:18, max:65})
9+
# ::category.method([...]) -> faker.helpers.arrayElement(["a","b"])
10+
# ::category.method([...], {...}) -> faker.helpers.arrayElements([...], {min:2})
911
#
10-
# Legacy syntax (still supported):
11-
# random_name -> Random name from built-in list
12-
# random_int(min,max) -> Random integer
13-
# current_timestamp -> ISO 8601 timestamp
12+
# Special values:
13+
# current_timestamp -> ISO 8601 timestamp
1414

15-
name: "faker:person.fullName"
16-
email: "faker:internet.email"
17-
age: "faker:number.int(18,65)"
15+
name: "::person.fullName"
16+
email: "::internet.email"
17+
age: "::number.int({min:18, max:65})"
1818
timestamp: "current_timestamp"
1919

2020
# More examples (commented out):
21-
# company: "faker:company.name"
22-
# address: "faker:location.streetAddress"
23-
# city: "faker:location.city"
24-
# phone: "faker:phone.number"
25-
# uuid: "faker:string.uuid"
26-
# productName: "faker:commerce.productName"
27-
# price: "faker:commerce.price(10,1000)"
21+
# company: "::company.name"
22+
# address: "::location.streetAddress"
23+
# city: "::location.city"
24+
# phone: "::phone.number"
25+
# uuid: "::string.uuid"
26+
# productName: "::commerce.productName"
27+
# price: "::commerce.price({min:10, max:1000})"
28+
# status: '::helpers.arrayElement(["active","pending","completed"])'

0 commit comments

Comments
 (0)